Snap for 11698527 from cd843a2c43d6e0b40f15499450669033df2d0e79 to mainline-appsearch-release
Change-Id: I9ee202ed31cc60613698ff2f5b5d58ec639a29d9
diff --git a/Cronet/tests/common/Android.bp b/Cronet/tests/common/Android.bp
index e17081a..703f544 100644
--- a/Cronet/tests/common/Android.bp
+++ b/Cronet/tests/common/Android.bp
@@ -17,6 +17,7 @@
// They must be fast and stable, and exercise public or test APIs.
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -28,7 +29,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 +41,9 @@
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",
+ "libnativecoverage",
],
+ data: [":cronet_javatests_resources"],
}
diff --git a/Cronet/tests/common/AndroidTest.xml b/Cronet/tests/common/AndroidTest.xml
index 2ac418f..ae6b65b 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,19 @@
<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" />
+ <!-- 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="orchestrator" value="true"/>
<option
name="device-listeners"
value="com.android.modules.utils.testing.NativeCoverageHackInstrumentationListener" />
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index 7b52694..92b73d9 100644
--- a/Cronet/tests/cts/Android.bp
+++ b/Cronet/tests/cts/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -46,7 +47,9 @@
"framework-connectivity",
"org.apache.http.legacy",
],
- lint: { test: true }
+ lint: {
+ test: true,
+ },
}
android_test {
diff --git a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
index 9fc4389..f86ac29 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
+++ b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
@@ -247,10 +247,8 @@
@Test
public void testHttpEngine_requestUsesDefaultUserAgent() throws Exception {
mEngine = mEngineBuilder.build();
- HttpCtsTestServer server =
- new HttpCtsTestServer(ApplicationProvider.getApplicationContext());
- String url = server.getUserAgentUrl();
+ String url = mTestServer.getUserAgentUrl();
UrlRequest request =
mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback).build();
request.start();
@@ -266,14 +264,12 @@
@Test
public void testHttpEngine_requestUsesCustomUserAgent() throws Exception {
String userAgent = "CtsTests User Agent";
- HttpCtsTestServer server =
- new HttpCtsTestServer(ApplicationProvider.getApplicationContext());
mEngine =
new HttpEngine.Builder(ApplicationProvider.getApplicationContext())
.setUserAgent(userAgent)
.build();
- String url = server.getUserAgentUrl();
+ String url = mTestServer.getUserAgentUrl();
UrlRequest request =
mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback).build();
request.start();
diff --git a/Cronet/tests/mts/Android.bp b/Cronet/tests/mts/Android.bp
index 63905c8..9486e1f 100644
--- a/Cronet/tests/mts/Android.bp
+++ b/Cronet/tests/mts/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -48,19 +49,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..5aed6559c 100644
--- a/Cronet/tests/mts/AndroidTest.xml
+++ b/Cronet/tests/mts/AndroidTest.xml
@@ -24,11 +24,28 @@
<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" />
+ <!-- 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="orchestrator" value="true"/>
</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/Cronet/tools/import/copy.bara.sky b/Cronet/tools/import/copy.bara.sky
deleted file mode 100644
index 61e3ba4..0000000
--- a/Cronet/tools/import/copy.bara.sky
+++ /dev/null
@@ -1,127 +0,0 @@
-# Copyright 2023 Google Inc. All rights reserved.
-#
-# 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.
-
-common_excludes = [
- # Exclude all Android build files
- "**/Android.bp",
- "**/Android.mk",
-
- # Exclude existing *OWNERS files
- "**/*OWNERS",
- "**/.git/**",
- "**/.gitignore",
-]
-
-cronet_origin_files = glob(
- include = [
- "base/**",
- "build/**",
- "build/buildflag.h",
- "chrome/VERSION",
- "components/cronet/**",
- "components/metrics/**",
- "components/nacl/**",
- "components/prefs/**",
- "crypto/**",
- "ipc/**",
- "net/**",
- # Note: Only used for tests.
- "testing/**",
- "url/**",
- "LICENSE",
- ],
- exclude = common_excludes + [
- # Per aosp/2367109
- "build/android/CheckInstallApk-debug.apk",
- "build/android/unused_resources/**",
- "build/linux/**",
-
- # Per aosp/2374766
- "components/cronet/ios/**",
- "components/cronet/native/**",
-
- # Per aosp/2399270
- "testing/buildbot/**",
-
- # Exclude all third-party directories. Those are specified explicitly
- # below, so no dependency can accidentally creep in.
- "**/third_party/**",
- ],
-) + glob(
- # Explicitly include third-party dependencies.
- # Note: some third-party dependencies include a third_party folder within
- # them. So far, this has not become a problem.
- include = [
- "base/third_party/cityhash/**",
- "base/third_party/cityhash_v103/**",
- "base/third_party/double_conversion/**",
- "base/third_party/dynamic_annotations/**",
- "base/third_party/icu/**",
- "base/third_party/nspr/**",
- "base/third_party/superfasthash/**",
- "base/third_party/valgrind/**",
- # Those are temporarily needed until Chromium finish the migration
- # of libc++[abi]
- "buildtools/third_party/libc++/**",
- "buildtools/third_party/libc++abi/**",
- # Note: Only used for tests.
- "net/third_party/nist-pkits/**",
- "net/third_party/quiche/**",
- "net/third_party/uri_template/**",
- "third_party/abseil-cpp/**",
- "third_party/android_ndk/sources/android/cpufeatures/**",
- "third_party/ashmem/**",
- "third_party/boringssl/**",
- "third_party/brotli/**",
- # Note: Only used for tests.
- "third_party/ced/**",
- "third_party/cpu_features/**",
- # Note: Only used for tests.
- "third_party/google_benchmark/**",
- # Note: Only used for tests.
- "third_party/googletest/**",
- "third_party/icu/**",
- "third_party/jni_zero/**",
- "third_party/libc++/**",
- "third_party/libc++abi/**",
- "third_party/libevent/**",
- # Note: Only used for tests.
- "third_party/libxml/**",
- # Note: Only used for tests.
- "third_party/lss/**",
- "third_party/metrics_proto/**",
- "third_party/modp_b64/**",
- "third_party/protobuf/**",
- # Note: Only used for tests.
- "third_party/quic_trace/**",
- # Note: Cronet currently uses Android's zlib
- # "third_party/zlib/**",
- "url/third_party/mozilla/**",
- ],
- exclude = common_excludes,
-)
-
-core.workflow(
- name = "import_cronet",
- authoring = authoring.overwrite("Cronet Mainline Eng <cronet-mainline-eng+copybara@google.com>"),
- # Origin folder is specified via source_ref argument, see import_cronet.sh
- origin = folder.origin(),
- origin_files = cronet_origin_files,
- destination = git.destination(
- # The destination URL is set by the invoking script.
- url = "overwritten/by/script",
- push = "upstream-import",
- ),
- mode = "SQUASH",
-)
diff --git a/Cronet/tools/import/import_cronet.sh b/Cronet/tools/import/import_cronet.sh
deleted file mode 100755
index 0f04af7..0000000
--- a/Cronet/tools/import/import_cronet.sh
+++ /dev/null
@@ -1,146 +0,0 @@
-#!/bin/bash
-
-# Copyright 2023 Google Inc. All rights reserved.
-#
-# 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.
-
-# Script to invoke copybara locally to import Cronet into Android.
-# Inputs:
-# Environment:
-# ANDROID_BUILD_TOP: path the root of the current Android directory.
-# Arguments:
-# -l rev: The last revision that was imported.
-# Optional Arguments:
-# -n rev: The new revision to import.
-# -f: Force copybara to ignore a failure to find the last imported revision.
-
-set -e -x
-
-OPTSTRING=fl:n:
-
-usage() {
- cat <<EOF
-Usage: import_cronet.sh -n new-rev [-l last-rev] [-f]
-EOF
- exit 1
-}
-
-COPYBARA_FOLDER_ORIGIN="/tmp/copybara-origin"
-
-#######################################
-# Create local upstream-import branch in external/cronet.
-# Globals:
-# ANDROID_BUILD_TOP
-# Arguments:
-# none
-#######################################
-setup_upstream_import_branch() {
- local git_dir="${ANDROID_BUILD_TOP}/external/cronet"
-
- (cd "${git_dir}" && git fetch aosp upstream-import:upstream-import)
-}
-
-#######################################
-# Setup folder.origin for copybara inside /tmp
-# Globals:
-# COPYBARA_FOLDER_ORIGIN
-# Arguments:
-# new_rev, string
-#######################################
-setup_folder_origin() (
- local _new_rev=$1
- mkdir -p "${COPYBARA_FOLDER_ORIGIN}"
- cd "${COPYBARA_FOLDER_ORIGIN}"
-
- if [ -d src ]; then
- (cd src && git fetch --tags && git checkout "${_new_rev}")
- else
- # For this to work _new_rev must be a branch or a tag.
- git clone --depth=1 --branch "${_new_rev}" https://chromium.googlesource.com/chromium/src.git
- fi
-
-
- cat <<EOF >.gclient
-solutions = [
- {
- "name": "src",
- "url": "https://chromium.googlesource.com/chromium/src.git",
- "managed": False,
- "custom_deps": {},
- "custom_vars": {},
- },
-]
-target_os = ["android"]
-EOF
- cd src
- # Set appropriate gclient flags to speed up syncing.
- gclient sync \
- --no-history \
- --shallow \
- --delete_unversioned_trees
-)
-
-#######################################
-# Runs the copybara import of Chromium
-# Globals:
-# ANDROID_BUILD_TOP
-# COPYBARA_FOLDER_ORIGIN
-# Arguments:
-# last_rev, string or empty
-# force, string or empty
-#######################################
-do_run_copybara() {
- local _last_rev=$1
- local _force=$2
-
- local -a flags
- flags+=(--git-destination-url="file://${ANDROID_BUILD_TOP}/external/cronet")
- flags+=(--repo-timeout 3m)
-
- # buildtools/third_party/libc++ contains an invalid symlink
- flags+=(--folder-origin-ignore-invalid-symlinks)
- flags+=(--git-no-verify)
-
- if [ ! -z "${_force}" ]; then
- flags+=(--force)
- fi
-
- if [ ! -z "${_last_rev}" ]; then
- flags+=(--last-rev "${_last_rev}")
- fi
-
- /google/bin/releases/copybara/public/copybara/copybara \
- "${flags[@]}" \
- "${ANDROID_BUILD_TOP}/packages/modules/Connectivity/Cronet/tools/import/copy.bara.sky" \
- import_cronet "${COPYBARA_FOLDER_ORIGIN}/src"
-}
-
-while getopts $OPTSTRING opt; do
- case "${opt}" in
- f) force=true ;;
- l) last_rev="${OPTARG}" ;;
- n) new_rev="${OPTARG}" ;;
- ?) usage ;;
- *) echo "'${opt}' '${OPTARG}'"
- esac
-done
-
-if [ -z "${new_rev}" ]; then
- echo "-n argument required"
- usage
-fi
-
-setup_upstream_import_branch
-setup_folder_origin "${new_rev}"
-do_run_copybara "${last_rev}" "${force}"
-
diff --git a/DnsResolver/Android.bp b/DnsResolver/Android.bp
index d133034..716eb10 100644
--- a/DnsResolver/Android.bp
+++ b/DnsResolver/Android.bp
@@ -14,6 +14,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -56,7 +57,10 @@
cc_test {
name: "dns_helper_unit_test",
defaults: ["netd_defaults"],
- test_suites: ["general-tests", "mts-tethering"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
test_config_template: ":net_native_test_config_template",
header_libs: [
"bpf_connectivity_headers",
@@ -68,8 +72,8 @@
"libcom.android.tethering.dns_helper",
],
shared_libs: [
- "libbase",
- "libcutils",
+ "libbase",
+ "libcutils",
],
compile_multilib: "both",
multilib: {
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/OWNERS_core_networking_xts b/OWNERS_core_networking_xts
index 7612210..b24e3ac 100644
--- a/OWNERS_core_networking_xts
+++ b/OWNERS_core_networking_xts
@@ -3,6 +3,8 @@
# For cherry-picks of CLs that are already merged in aosp/master, or flaky test fixes.
jchalard@google.com #{LAST_RESORT_SUGGESTION}
+# In addition to cherry-picks and flaky test fixes, also for APF firmware tests
+# (to verify correct behaviour of the wifi APF interpreter)
maze@google.com #{LAST_RESORT_SUGGESTION}
# In addition to cherry-picks and flaky test fixes, also for incremental changes on NsdManager tests
# to increase coverage for existing behavior, and testing of bug fixes in NsdManager
diff --git a/TEST_MAPPING b/TEST_MAPPING
index ab3ed66..d8d4c21 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -246,6 +246,9 @@
},
{
"exclude-annotation": "com.android.testutils.DnsResolverModuleTest"
+ },
+ {
+ "exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
}
]
},
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 73c11ba..19bcff9 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -73,6 +74,8 @@
"net-utils-device-common-bpf",
"net-utils-device-common-ip",
"net-utils-device-common-netlink",
+ "net-utils-device-common-struct",
+ "net-utils-device-common-struct-base",
"netd-client",
"tetheringstatsprotos",
],
@@ -82,7 +85,6 @@
],
manifest: "AndroidManifestBase.xml",
lint: {
- strict_updatability_linting: true,
error_checks: ["NewApi"],
},
}
@@ -98,11 +100,9 @@
],
static_libs: [
"NetworkStackApiCurrentShims",
- "net-utils-device-common-struct",
],
apex_available: ["com.android.tethering"],
lint: {
- strict_updatability_linting: true,
baseline_filename: "lint-baseline.xml",
},
}
@@ -116,11 +116,9 @@
],
static_libs: [
"NetworkStackApiStableShims",
- "net-utils-device-common-struct",
],
apex_available: ["com.android.tethering"],
lint: {
- strict_updatability_linting: true,
baseline_filename: "lint-baseline.xml",
},
}
@@ -195,9 +193,6 @@
optimize: {
proguard_flags_files: ["proguard.flags"],
},
- lint: {
- strict_updatability_linting: true,
- },
}
// Updatable tethering packaged for finalized API
@@ -213,10 +208,6 @@
use_embedded_native_libs: true,
privapp_allowlist: ":privapp_allowlist_com.android.tethering",
apex_available: ["com.android.tethering"],
- lint: {
- strict_updatability_linting: true,
- baseline_filename: "lint-baseline.xml",
- },
}
android_app {
@@ -233,9 +224,7 @@
privapp_allowlist: ":privapp_allowlist_com.android.tethering",
apex_available: ["com.android.tethering"],
lint: {
- strict_updatability_linting: true,
error_checks: ["NewApi"],
- baseline_filename: "lint-baseline.xml",
},
}
@@ -263,9 +252,6 @@
static_libs: ["tetheringprotos"],
apex_available: ["com.android.tethering"],
min_sdk_version: "30",
- lint: {
- baseline_filename: "lint-baseline.xml",
- },
}
genrule {
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 1006238..047ba02 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -50,16 +51,6 @@
"//external/cronet/third_party/boringssl:libcrypto",
"//external/cronet/third_party/boringssl:libssl",
],
- arch: {
- riscv64: {
- // TODO: remove this when there is a riscv64 libcronet
- exclude_jni_libs: [
- "cronet_aml_components_cronet_android_cronet",
- "//external/cronet/third_party/boringssl:libcrypto",
- "//external/cronet/third_party/boringssl:libssl",
- ],
- },
- },
}
apex {
@@ -92,7 +83,7 @@
both: {
jni_libs: [
"libframework-connectivity-jni",
- "libframework-connectivity-tiramisu-jni"
+ "libframework-connectivity-tiramisu-jni",
],
},
},
@@ -109,16 +100,17 @@
"dscpPolicy.o",
"netd.o",
"offload.o",
- "offload@btf.o",
+ "offload@mainline.o",
"test.o",
- "test@btf.o",
+ "test@mainline.o",
],
apps: [
"ServiceConnectivityResources",
],
prebuilts: [
- "ot-daemon.init.34rc",
"current_sdkinfo",
+ "netbpfload.mainline.rc",
+ "ot-daemon.init.34rc",
],
manifest: "manifest.json",
key: "com.android.tethering.key",
diff --git a/Tethering/apex/permissions/Android.bp b/Tethering/apex/permissions/Android.bp
index 69c1aa2..20772a8 100644
--- a/Tethering/apex/permissions/Android.bp
+++ b/Tethering/apex/permissions/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
default_visibility: ["//packages/modules/Connectivity/Tethering:__subpackages__"],
}
@@ -22,4 +23,4 @@
filegroup {
name: "privapp_allowlist_com.android.tethering",
srcs: ["permissions.xml"],
-}
\ No newline at end of file
+}
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index bcea425..9fa073b 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -14,6 +14,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -35,7 +36,7 @@
"//frameworks/base/core/tests/bandwidthtests",
"//frameworks/base/core/tests/benchmarks",
"//frameworks/base/core/tests/utillib",
- "//frameworks/base/packages/Connectivity/tests:__subpackages__",
+ "//frameworks/base/services/tests/VpnTests",
"//frameworks/base/tests/vcn",
"//frameworks/opt/telephony/tests/telephonytests",
"//packages/modules/CaptivePortalLogin/tests",
@@ -55,14 +56,19 @@
hostdex: true, // for hiddenapi check
permitted_packages: ["android.net"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
+ aconfig_declarations: [
+ "com.android.net.flags-aconfig",
+ ],
}
java_library {
- name: "framework-tethering-pre-jarjar",
- defaults: [
- "framework-tethering-defaults",
- ],
+ name: "framework-tethering-pre-jarjar",
+ defaults: [
+ "framework-tethering-defaults",
+ ],
}
java_genrule {
@@ -88,7 +94,7 @@
name: "framework-tethering-defaults",
defaults: ["framework-module-defaults"],
srcs: [
- ":framework-tethering-srcs"
+ ":framework-tethering-srcs",
],
libs: ["framework-connectivity.stubs.module_lib"],
aidl: {
@@ -107,5 +113,5 @@
"src/**/*.aidl",
"src/**/*.java",
],
- path: "src"
+ path: "src",
}
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index 844ff64..a287b42 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -95,13 +95,16 @@
method public default void onUpstreamChanged(@Nullable android.net.Network);
}
- public static class TetheringManager.TetheringRequest {
+ public static final class TetheringManager.TetheringRequest implements android.os.Parcelable {
+ method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") public int describeContents();
method @Nullable public android.net.LinkAddress getClientStaticIpv4Address();
method public int getConnectivityScope();
method @Nullable public android.net.LinkAddress getLocalIpv4Address();
method public boolean getShouldShowEntitlementUi();
method public int getTetheringType();
method public boolean isExemptFromEntitlementCheck();
+ method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringManager.TetheringRequest> CREATOR;
}
public static class TetheringManager.TetheringRequest.Builder {
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index cd914d3..7b769d4 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -18,6 +18,7 @@
import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -28,6 +29,8 @@
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.util.ArrayMap;
@@ -59,6 +62,14 @@
*/
@SystemApi
public class TetheringManager {
+ // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+ // available here
+ /** @hide */
+ public static class Flags {
+ static final String TETHERING_REQUEST_WITH_SOFT_AP_CONFIG =
+ "com.android.net.flags.tethering_request_with_soft_ap_config";
+ }
+
private static final String TAG = TetheringManager.class.getSimpleName();
private static final int DEFAULT_TIMEOUT_MS = 60_000;
private static final long CONNECTOR_POLL_INTERVAL_MILLIS = 200L;
@@ -673,14 +684,44 @@
/**
* Use with {@link #startTethering} to specify additional parameters when starting tethering.
*/
- public static class TetheringRequest {
+ public static final class TetheringRequest implements Parcelable {
/** A configuration set for TetheringRequest. */
private final TetheringRequestParcel mRequestParcel;
- private TetheringRequest(final TetheringRequestParcel request) {
+ private TetheringRequest(@NonNull final TetheringRequestParcel request) {
mRequestParcel = request;
}
+ private TetheringRequest(@NonNull Parcel in) {
+ mRequestParcel = in.readParcelable(TetheringRequestParcel.class.getClassLoader());
+ }
+
+ @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @NonNull
+ public static final Creator<TetheringRequest> CREATOR = new Creator<>() {
+ @Override
+ public TetheringRequest createFromParcel(@NonNull Parcel in) {
+ return new TetheringRequest(in);
+ }
+
+ @Override
+ public TetheringRequest[] newArray(int size) {
+ return new TetheringRequest[size];
+ }
+ };
+
+ @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeParcelable(mRequestParcel, flags);
+ }
+
/** Builder used to create TetheringRequest. */
public static class Builder {
private final TetheringRequestParcel mBuilderParcel;
diff --git a/Tethering/lint-baseline.xml b/Tethering/lint-baseline.xml
index 37511c6..4f92c9c 100644
--- a/Tethering/lint-baseline.xml
+++ b/Tethering/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
<issue
id="NewApi"
@@ -8,7 +8,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/OffloadController.java"
- line="293"
+ line="283"
column="44"/>
</issue>
diff --git a/Tethering/res/values-mcc310-mnc004-eu/strings.xml b/Tethering/res/values-mcc310-mnc004-eu/strings.xml
index c970dd7..ff2a505 100644
--- a/Tethering/res/values-mcc310-mnc004-eu/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-eu/strings.xml
@@ -18,7 +18,7 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="no_upstream_notification_title" msgid="3584617491053416666">"Konexioa partekatzeko aukerak ez du Interneteko konexiorik"</string>
<string name="no_upstream_notification_message" msgid="5626323795587558017">"Ezin dira konektatu gailuak"</string>
- <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Desaktibatu konexioa partekatzeko aukera"</string>
+ <string name="no_upstream_notification_disable_button" msgid="868677179945695858">"Desaktibatu konexioa partekatzea"</string>
<string name="upstream_roaming_notification_title" msgid="2870229486619751829">"Wifi-gunea edo konexioa partekatzeko aukera aktibatuta dago"</string>
<string name="upstream_roaming_notification_message" msgid="5229740963392849544">"Baliteke tarifa gehigarriak ordaindu behar izatea ibiltaritza erabili bitartean"</string>
</resources>
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index a8c8408..9e0c970 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -33,6 +33,7 @@
import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
+import static com.android.networkstack.tethering.TetheringConfiguration.USE_SYNC_SM;
import static com.android.networkstack.tethering.UpstreamNetworkState.isVcnInterface;
import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_IPSERVER;
@@ -59,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;
@@ -135,8 +137,8 @@
private static final MacAddress NULL_MAC_ADDRESS = MacAddress.fromString("00:00:00:00:00:00");
private static final String TAG = "IpServer";
- private static final boolean DBG = false;
- private static final boolean VDBG = false;
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
private static final Class[] sMessageClasses = {
IpServer.class
};
@@ -315,7 +317,6 @@
private final TetheringMetrics mTetheringMetrics;
private final Handler mHandler;
- private final boolean mIsSyncSM;
// TODO: Add a dependency object to pass the data members or variables from the tethering
// object. It helps to reduce the arguments of the constructor.
@@ -325,7 +326,7 @@
@Nullable LateSdk<RoutingCoordinatorManager> routingCoordinator, Callback callback,
TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
TetheringMetrics tetheringMetrics, Dependencies deps) {
- super(ifaceName, config.isSyncSM() ? null : handler.getLooper());
+ super(ifaceName, USE_SYNC_SM ? null : handler.getLooper());
mHandler = handler;
mLog = log.forSubComponent(ifaceName);
mNetd = netd;
@@ -338,7 +339,6 @@
mLinkProperties = new LinkProperties();
mUsingLegacyDhcp = config.useLegacyDhcpServer();
mP2pLeasesSubnetPrefixLength = config.getP2pLeasesSubnetPrefixLength();
- mIsSyncSM = config.isSyncSM();
mPrivateAddressCoordinator = addressCoordinator;
mDeps = deps;
mTetheringMetrics = tetheringMetrics;
@@ -516,7 +516,7 @@
private void handleError() {
mLastError = TETHER_ERROR_DHCPSERVER_ERROR;
- if (mIsSyncSM) {
+ if (USE_SYNC_SM) {
sendMessage(CMD_SERVICE_FAILED_TO_START, TETHER_ERROR_DHCPSERVER_ERROR);
} else {
sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START,
@@ -900,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));
@@ -908,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);
}
@@ -920,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();
@@ -941,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);
}
@@ -1171,7 +1171,7 @@
// in previous versions of the mainline module.
// TODO : remove sendMessageAtFrontOfQueueToAsyncSM after migrating to the Sync
// StateMachine.
- if (mIsSyncSM) {
+ if (USE_SYNC_SM) {
sendSelfMessageToSyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
} else {
sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
@@ -1548,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));
@@ -1579,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));
@@ -1589,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/OffloadHardwareInterface.java b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
index 53c80ae..13a7a22 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
@@ -16,6 +16,7 @@
package com.android.networkstack.tethering;
+import static com.android.net.module.util.netlink.NetlinkUtils.SOCKET_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;
@@ -198,7 +199,8 @@
public NativeHandle createConntrackSocket(final int groups) {
final FileDescriptor fd;
try {
- fd = NetlinkUtils.netlinkSocketForProto(OsConstants.NETLINK_NETFILTER);
+ fd = NetlinkUtils.netlinkSocketForProto(OsConstants.NETLINK_NETFILTER,
+ SOCKET_RECV_BUFSIZE);
} catch (ErrnoException e) {
mLog.e("Unable to create conntrack socket " + 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..d85d92f 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;
/**
*
@@ -175,8 +173,8 @@
public class Tethering {
private static final String TAG = Tethering.class.getSimpleName();
- private static final boolean DBG = false;
- private static final boolean VDBG = false;
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
private static final Class[] sMessageClasses = {
Tethering.class, TetherMainSM.class, IpServer.class
@@ -243,9 +241,6 @@
private final TetherMainSM mTetherMainSM;
private final OffloadController mOffloadController;
private final UpstreamNetworkMonitor mUpstreamNetworkMonitor;
- // TODO: Figure out how to merge this and other downstream-tracking objects
- // into a single coherent structure.
- private final HashSet<IpServer> mForwardedDownstreams;
private final VersionedBroadcastListener mCarrierConfigChange;
private final TetheringDependencies mDeps;
private final EntitlementManager mEntitlementMgr;
@@ -273,8 +268,6 @@
private boolean mRndisEnabled; // track the RNDIS function enabled state
private boolean mNcmEnabled; // track the NCM function enabled state
- // True iff. WiFi tethering should be started when soft AP is ready.
- private boolean mWifiTetherRequested;
private Network mTetherUpstream;
private TetherStatesParcel mTetherStatesParcel;
private boolean mDataSaverEnabled = false;
@@ -331,7 +324,6 @@
(what, obj) -> {
mTetherMainSM.sendMessage(TetherMainSM.EVENT_UPSTREAM_CALLBACK, what, 0, obj);
});
- mForwardedDownstreams = new HashSet<>();
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_CARRIER_CONFIG_CHANGED);
@@ -370,6 +362,7 @@
// Load tethering configuration.
updateConfiguration();
+ mConfig.readEnableSyncSM(mContext);
// It is OK for the configuration to be passed to the PrivateAddressCoordinator at
// construction time because the only part of the configuration it uses is
// shouldEnableWifiP2pDedicatedIp(), and currently do not support changing that.
@@ -764,7 +757,6 @@
}
if ((enable && mgr.startTetheredHotspot(null /* use existing softap config */))
|| (!enable && mgr.stopSoftAp())) {
- mWifiTetherRequested = enable;
return TETHER_ERROR_NO_ERROR;
}
} finally {
@@ -1471,10 +1463,6 @@
}
private void disableWifiIpServing(String ifname, int apState) {
- // Regardless of whether we requested this transition, the AP has gone
- // down. Don't try to tether again unless we're requested to do so.
- mWifiTetherRequested = false;
-
mLog.log("Canceling WiFi tethering request - interface=" + ifname + " state=" + apState);
disableWifiIpServingCommon(TETHERING_WIFI, ifname);
@@ -1506,8 +1494,7 @@
private void enableWifiIpServing(String ifname, int wifiIpMode) {
mLog.log("request WiFi tethering - interface=" + ifname + " state=" + wifiIpMode);
- // Map wifiIpMode values to IpServer.Callback serving states, inferring
- // from mWifiTetherRequested as a final "best guess".
+ // Map wifiIpMode values to IpServer.Callback serving states.
final int ipServingMode;
switch (wifiIpMode) {
case IFACE_IP_MODE_TETHERED:
@@ -1654,11 +1641,6 @@
mLog.log(state.getName() + " got " + sMagicDecoderRing.get(what, Integer.toString(what)));
}
- private boolean upstreamWanted() {
- if (!mForwardedDownstreams.isEmpty()) return true;
- return mWifiTetherRequested;
- }
-
// Needed because the canonical source of upstream truth is just the
// upstream interface set, |mCurrentUpstreamIfaceSet|.
private boolean pertainsToCurrentUpstream(UpstreamNetworkState ns) {
@@ -1716,12 +1698,16 @@
private final ArrayList<IpServer> mNotifyList;
private final IPv6TetheringCoordinator mIPv6TetheringCoordinator;
private final OffloadWrapper mOffload;
+ // TODO: Figure out how to merge this and other downstream-tracking objects
+ // into a single coherent structure.
+ private final HashSet<IpServer> mForwardedDownstreams;
private static final int UPSTREAM_SETTLE_TIME_MS = 10000;
TetherMainSM(String name, Looper looper, TetheringDependencies deps) {
super(name, looper);
+ mForwardedDownstreams = new HashSet<>();
mInitialState = new InitialState();
mTetherModeAliveState = new TetherModeAliveState();
mSetIpForwardingEnabledErrorState = new SetIpForwardingEnabledErrorState();
@@ -2057,6 +2043,10 @@
}
}
+ private boolean upstreamWanted() {
+ return !mForwardedDownstreams.isEmpty();
+ }
+
class TetherModeAliveState extends State {
boolean mUpstreamWanted = false;
boolean mTryCell = true;
@@ -2394,6 +2384,9 @@
hasCallingPermission(NETWORK_SETTINGS)
|| hasCallingPermission(PERMISSION_MAINLINE_NETWORK_STACK)
|| hasCallingPermission(NETWORK_STACK);
+ if (callback == null) {
+ throw new NullPointerException();
+ }
mHandler.post(() -> {
mTetheringEventCallbacks.register(callback, new CallbackCookie(hasListPermission));
final TetheringCallbackStartedParcel parcel = new TetheringCallbackStartedParcel();
@@ -2652,7 +2645,7 @@
}
pw.println(" - lastError = " + tetherState.lastError);
}
- pw.println("Upstream wanted: " + upstreamWanted());
+ pw.println("Upstream wanted: " + mTetherMainSM.upstreamWanted());
pw.println("Current upstream interface(s): " + mCurrentUpstreamIfaceSet);
pw.decreaseIndent();
@@ -2694,31 +2687,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/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index d09183a..298940e 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -141,6 +141,9 @@
*/
public static final int DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS = 5000;
+ /** A flag for using synchronous or asynchronous state machine. */
+ public static boolean USE_SYNC_SM = false;
+
public final String[] tetherableUsbRegexs;
public final String[] tetherableWifiRegexs;
public final String[] tetherableWigigRegexs;
@@ -174,7 +177,6 @@
private final boolean mEnableWearTethering;
private final boolean mRandomPrefixBase;
- private final boolean mEnableSyncSm;
private final int mUsbTetheringFunction;
protected final ContentResolver mContentResolver;
@@ -293,7 +295,6 @@
mEnableWearTethering = shouldEnableWearTethering(ctx);
mRandomPrefixBase = mDeps.isFeatureEnabled(ctx, TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION);
- mEnableSyncSm = mDeps.isFeatureEnabled(ctx, TETHER_ENABLE_SYNC_SM);
configLog.log(toString());
}
@@ -387,8 +388,14 @@
return mRandomPrefixBase;
}
- public boolean isSyncSM() {
- return mEnableSyncSm;
+ /**
+ * Check whether sync SM is enabled then set it to USE_SYNC_SM. This should be called once
+ * when tethering is created. Otherwise if the flag is pushed while tethering is enabled,
+ * then it's possible for some IpServer(s) running the new sync state machine while others
+ * use the async state machine.
+ */
+ public void readEnableSyncSM(final Context ctx) {
+ USE_SYNC_SM = mDeps.isFeatureEnabled(ctx, TETHER_ENABLE_SYNC_SM);
}
/** Does the dumping.*/
@@ -445,8 +452,8 @@
pw.print("mRandomPrefixBase: ");
pw.println(mRandomPrefixBase);
- pw.print("mEnableSyncSm: ");
- pw.println(mEnableSyncSm);
+ pw.print("USE_SYNC_SM: ");
+ pw.println(USE_SYNC_SM);
}
/** Returns the string representation of this object.*/
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 9dfd225..3f86056 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -156,6 +156,7 @@
/**
* Get a reference to BluetoothAdapter to be used by tethering.
*/
+ @Nullable
public abstract BluetoothAdapter getBluetoothAdapter();
/**
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index aa73819..623f502 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -30,6 +30,7 @@
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.Intent;
import android.net.IIntResultListener;
@@ -377,7 +378,11 @@
@Override
public BluetoothAdapter getBluetoothAdapter() {
- return BluetoothAdapter.getDefaultAdapter();
+ final BluetoothManager btManager = getSystemService(BluetoothManager.class);
+ if (btManager == null) {
+ return null;
+ }
+ return btManager.getAdapter();
}
};
}
diff --git a/Tethering/tests/Android.bp b/Tethering/tests/Android.bp
index 72ca666..22cf3c5 100644
--- a/Tethering/tests/Android.bp
+++ b/Tethering/tests/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -24,5 +25,5 @@
visibility: [
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
- ]
+ ],
}
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index 2594a5e..337d408 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -14,6 +14,7 @@
// limitations under the License.
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -32,6 +33,7 @@
"net-tests-utils",
"net-utils-device-common",
"net-utils-device-common-bpf",
+ "net-utils-device-common-struct-base",
"testables",
"connectivity-net-module-utils-bpf",
],
@@ -45,12 +47,11 @@
android_library {
name: "TetheringIntegrationTestsBaseLib",
target_sdk_version: "current",
- platform_apis: true,
defaults: ["TetheringIntegrationTestsDefaults"],
visibility: [
"//packages/modules/Connectivity/Tethering/tests/mts",
"//packages/modules/Connectivity/tests/cts/net",
- ]
+ ],
}
// Library including tethering integration tests targeting the latest stable SDK.
@@ -58,7 +59,6 @@
android_library {
name: "TetheringIntegrationTestsLatestSdkLib",
target_sdk_version: "33",
- platform_apis: true,
defaults: ["TetheringIntegrationTestsDefaults"],
srcs: [
"src/**/*.java",
@@ -67,7 +67,7 @@
"//packages/modules/Connectivity/tests/cts/tethering",
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
- ]
+ ],
}
// Library including tethering integration tests targeting current development SDK.
@@ -75,7 +75,6 @@
android_library {
name: "TetheringIntegrationTestsLib",
target_sdk_version: "current",
- platform_apis: true,
defaults: ["TetheringIntegrationTestsDefaults"],
srcs: [
"src/**/*.java",
@@ -83,7 +82,7 @@
visibility: [
"//packages/modules/Connectivity/tests/cts/tethering",
"//packages/modules/Connectivity/Tethering/tests/mts",
- ]
+ ],
}
// TODO: remove because TetheringIntegrationTests has been covered by ConnectivityCoverageTests.
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index c232697..120b871 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -74,7 +74,6 @@
import org.junit.After;
import org.junit.Before;
-import org.junit.BeforeClass;
import java.io.FileDescriptor;
import java.net.Inet4Address;
@@ -83,8 +82,10 @@
import java.net.NetworkInterface;
import java.net.SocketException;
import java.nio.ByteBuffer;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -142,10 +143,12 @@
private static final Context sContext =
InstrumentationRegistry.getInstrumentation().getContext();
- private static final EthernetManager sEm = sContext.getSystemService(EthernetManager.class);
+ protected 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);
+ private static final List<String> sCallbackErrors =
+ Collections.synchronizedList(new ArrayList<>());
// Late initialization in setUp()
private boolean mRunTests;
@@ -164,33 +167,6 @@
return sContext;
}
- @BeforeClass
- public static void setUpOnce() throws Exception {
- // The first test case may experience tethering restart with IP conflict handling.
- // 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.
- MyTetheringEventCallback callback = null;
- TestNetworkInterface testIface = null;
- try {
- // If the physical ethernet interface is available, do nothing.
- if (isInterfaceForTetheringAvailable()) return;
-
- testIface = createTestInterface();
- setIncludeTestInterfaces(true);
-
- callback = enableEthernetTethering(testIface.getInterfaceName(), null);
- callback.awaitUpstreamChanged(true /* throwTimeoutException */);
- } catch (TimeoutException e) {
- Log.d(TAG, "WARNNING " + e);
- } finally {
- maybeCloseTestInterface(testIface);
- maybeUnregisterTetheringEventCallback(callback);
-
- setIncludeTestInterfaces(false);
- }
- }
-
@Before
public void setUp() throws Exception {
mHandlerThread = new HandlerThread(getClass().getSimpleName());
@@ -201,6 +177,7 @@
assumeTrue(mRunTests);
mTetheredInterfaceRequester = new TetheredInterfaceRequester();
+ sCallbackErrors.clear();
}
private boolean isEthernetTetheringSupported() throws Exception {
@@ -280,6 +257,10 @@
mHandlerThread.quitSafely();
mHandlerThread.join();
}
+
+ if (sCallbackErrors.size() > 0) {
+ fail("Some callbacks had errors: " + sCallbackErrors);
+ }
}
protected static boolean isInterfaceForTetheringAvailable() throws Exception {
@@ -391,7 +372,7 @@
}
@Override
public void onTetheredInterfacesChanged(List<String> interfaces) {
- fail("Should only call callback that takes a Set<TetheringInterface>");
+ addCallbackError("Should only call callback that takes a Set<TetheringInterface>");
}
@Override
@@ -412,7 +393,7 @@
@Override
public void onLocalOnlyInterfacesChanged(List<String> interfaces) {
- fail("Should only call callback that takes a Set<TetheringInterface>");
+ addCallbackError("Should only call callback that takes a Set<TetheringInterface>");
}
@Override
@@ -481,7 +462,7 @@
// Ignore stale callbacks registered by previous test cases.
if (mUnregistered) return;
- fail("TetheringEventCallback got error:" + error + " on iface " + ifName);
+ addCallbackError("TetheringEventCallback got error:" + error + " on iface " + ifName);
}
@Override
@@ -536,6 +517,11 @@
}
}
+ private static void addCallbackError(String error) {
+ Log.e(TAG, error);
+ sCallbackErrors.add(error);
+ }
+
protected static MyTetheringEventCallback enableEthernetTethering(String iface,
TetheringRequest request, Network expectedUpstream) throws Exception {
// Enable ethernet tethering with null expectedUpstream means the test accept any upstream
@@ -562,7 +548,7 @@
@Override
public void onTetheringFailed(int resultCode) {
- fail("Unexpectedly got onTetheringFailed");
+ addCallbackError("Unexpectedly got onTetheringFailed");
}
};
Log.d(TAG, "Starting Ethernet tethering");
@@ -865,12 +851,12 @@
// trigger the reselection, the total test time may over test suite 1 minmute timeout.
// Probably need to disable/restore all internet networks in a common place of test
// process. Currently, EthernetTetheringTest is part of CTS test which needs wifi network
- // connection if device has wifi feature. CtsNetUtils#toggleWifi() checks wifi connection
- // during the toggling process.
- // See Tethering#chooseUpstreamType, CtsNetUtils#toggleWifi.
+ // connection if device has wifi feature.
+ // See Tethering#chooseUpstreamType
// TODO: toggle cellular network if the device has no WIFI feature.
Log.d(TAG, "Toggle WIFI to retry upstream selection");
- sCtsNetUtils.toggleWifi();
+ sCtsNetUtils.disableWifi();
+ sCtsNetUtils.ensureWifiConnected();
// Wait for expected upstream.
final CompletableFuture<Network> future = new CompletableFuture<>();
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 4949eaa..c54d1b4 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -59,6 +59,7 @@
import com.android.testutils.NetworkStackModuleTest;
import com.android.testutils.TapPacketReader;
+import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -150,6 +151,35 @@
(byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04 /* Address: 1.2.3.4 */
};
+ /** Enable/disable tethering once before running the tests. */
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ // The first test case may experience tethering restart with IP conflict handling.
+ // 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.
+ MyTetheringEventCallback callback = null;
+ TestNetworkInterface testIface = null;
+ assumeTrue(sEm != null);
+ try {
+ // If the physical ethernet interface is available, do nothing.
+ if (isInterfaceForTetheringAvailable()) return;
+
+ testIface = createTestInterface();
+ setIncludeTestInterfaces(true);
+
+ callback = enableEthernetTethering(testIface.getInterfaceName(), null);
+ callback.awaitUpstreamChanged(true /* throwTimeoutException */);
+ } catch (TimeoutException e) {
+ Log.d(TAG, "WARNNING " + e);
+ } finally {
+ maybeCloseTestInterface(testIface);
+ maybeUnregisterTetheringEventCallback(callback);
+
+ setIncludeTestInterfaces(false);
+ }
+ }
+
@Test
public void testVirtualEthernetAlreadyExists() throws Exception {
// This test requires manipulating packets. Skip if there is a physical Ethernet connected.
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
index 4f4b03c..c4d5636 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -44,6 +45,7 @@
"junit-params",
"connectivity-net-module-utils-bpf",
"net-utils-device-common-bpf",
+ "net-utils-device-common-struct-base",
],
jni_libs: [
diff --git a/Tethering/tests/privileged/Android.bp b/Tethering/tests/privileged/Android.bp
index c890197..ba6be66 100644
--- a/Tethering/tests/privileged/Android.bp
+++ b/Tethering/tests/privileged/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
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/Android.bp b/Tethering/tests/unit/Android.bp
index 36d9a63..24407ca 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -16,6 +16,7 @@
// Tests in this folder are included both in unit tests and CTS.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -23,7 +24,7 @@
name: "TetheringCommonTests",
srcs: [
"common/**/*.java",
- "common/**/*.kt"
+ "common/**/*.kt",
],
static_libs: [
"androidx.test.rules",
@@ -95,7 +96,7 @@
visibility: [
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
- ]
+ ],
}
android_test {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index 19c6e5a..dd51c7a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -757,23 +757,24 @@
private void setTetherEnableSyncSMFlagEnabled(Boolean enabled) {
mDeps.setFeatureEnabled(TetheringConfiguration.TETHER_ENABLE_SYNC_SM, enabled);
+ new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps).readEnableSyncSM(mMockContext);
}
- private void assertEnableSyncSMIs(boolean value) {
- assertEquals(value, new TetheringConfiguration(
- mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps).isSyncSM());
+ private void assertEnableSyncSM(boolean value) {
+ assertEquals(value, TetheringConfiguration.USE_SYNC_SM);
}
@Test
public void testEnableSyncSMFlag() throws Exception {
// Test default disabled
setTetherEnableSyncSMFlagEnabled(null);
- assertEnableSyncSMIs(false);
+ assertEnableSyncSM(false);
setTetherEnableSyncSMFlagEnabled(true);
- assertEnableSyncSMIs(true);
+ assertEnableSyncSM(true);
setTetherEnableSyncSMFlagEnabled(false);
- assertEnableSyncSMIs(false);
+ assertEnableSyncSM(false);
}
}
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..9f430af 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
@@ -3624,6 +3622,42 @@
InetAddresses.parseNumericAddress(ifaceConfig.ipv4Addr), ifaceConfig.prefixLength);
assertFalse(sapPrefix.equals(lohsPrefix));
}
+
+ @Test
+ public void testWifiTetheringWhenP2pActive() throws Exception {
+ initTetheringOnTestThread();
+ // Enable wifi P2P.
+ sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
+ verifyInterfaceServingModeStarted(TEST_P2P_IFNAME);
+ verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
+ verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+ // Verify never enable upstream if only P2P active.
+ verify(mUpstreamNetworkMonitor, never()).setTryCell(true);
+ assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastErrorForTest(TEST_P2P_IFNAME));
+
+ when(mWifiManager.startTetheredHotspot(any())).thenReturn(true);
+ // Emulate pressing the WiFi tethering button.
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+ null);
+ mLooper.dispatchAll();
+ verify(mWifiManager).startTetheredHotspot(null);
+ verifyNoMoreInteractions(mWifiManager);
+
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+ verify(mWifiManager).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+
+ verify(mWifiManager).updateInterfaceIpState(TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ verifyNoMoreInteractions(mWifiManager);
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_ACTIVE_TETHER);
+
+ verify(mUpstreamNetworkMonitor).setTryCell(true);
+ }
+
// TODO: Test that a request for hotspot mode doesn't interfere with an
// already operating tethering mode interface.
}
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index cdf47e7..1958aa8 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -18,6 +18,7 @@
// struct definitions shared with JNI
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -93,13 +94,13 @@
}
bpf {
- name: "offload@btf.o",
- srcs: ["offload@btf.c"],
+ name: "offload@mainline.o",
+ srcs: ["offload@mainline.c"],
btf: true,
cflags: [
"-Wall",
"-Werror",
- "-DBTF",
+ "-DMAINLINE",
],
}
@@ -113,13 +114,13 @@
}
bpf {
- name: "test@btf.o",
- srcs: ["test@btf.c"],
+ name: "test@mainline.o",
+ srcs: ["test@mainline.c"],
btf: true,
cflags: [
"-Wall",
"-Werror",
- "-DBTF",
+ "-DMAINLINE",
],
}
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
index 0a2b0b8..152dda6 100644
--- a/bpf_progs/block.c
+++ b/bpf_progs/block.c
@@ -19,8 +19,8 @@
#include <netinet/in.h>
#include <stdint.h>
-// The resulting .o needs to load on the Android T bpfloader
-#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
+// The resulting .o needs to load on Android T+
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
#include "bpf_helpers.h"
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index addb02f..f83e5ae 100644
--- a/bpf_progs/clatd.c
+++ b/bpf_progs/clatd.c
@@ -30,8 +30,8 @@
#define __kernel_udphdr udphdr
#include <linux/udp.h>
-// The resulting .o needs to load on the Android T bpfloader
-#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
+// The resulting .o needs to load on Android T+
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
#include "bpf_helpers.h"
#include "bpf_net_helpers.h"
@@ -265,6 +265,10 @@
*(struct iphdr*)data = ip;
}
+ // Count successfully translated packet
+ __sync_fetch_and_add(&v->packets, 1);
+ __sync_fetch_and_add(&v->bytes, skb->len - l2_header_size);
+
// Redirect, possibly back to same interface, so tcpdump sees packet twice.
if (v->oif) return bpf_redirect(v->oif, BPF_F_INGRESS);
@@ -416,6 +420,10 @@
// Copy over the new ipv6 header without an ethernet header.
*(struct ipv6hdr*)data = ip6;
+ // Count successfully translated packet
+ __sync_fetch_and_add(&v->packets, 1);
+ __sync_fetch_and_add(&v->bytes, skb->len);
+
// Redirect to non v4-* interface. Tcpdump only sees packet after this redirect.
return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
}
diff --git a/bpf_progs/clatd.h b/bpf_progs/clatd.h
index b5f1cdc..a75798f 100644
--- a/bpf_progs/clatd.h
+++ b/bpf_progs/clatd.h
@@ -39,8 +39,10 @@
typedef struct {
uint32_t oif; // The output interface to redirect to (0 means don't redirect)
struct in_addr local4; // The destination IPv4 address
+ uint64_t packets; // Count of translated gso (large) packets
+ uint64_t bytes; // Sum of post-translation skb->len
} ClatIngress6Value;
-STRUCT_SIZE(ClatIngress6Value, 4 + 4); // 8
+STRUCT_SIZE(ClatIngress6Value, 4 + 4 + 8 + 8); // 24
typedef struct {
uint32_t iif; // The input interface index
@@ -54,7 +56,9 @@
struct in6_addr pfx96; // The destination /96 nat64 prefix, bottom 32 bits must be 0
bool oifIsEthernet; // Whether the output interface requires ethernet header
uint8_t pad[3];
+ uint64_t packets; // Count of translated gso (large) packets
+ uint64_t bytes; // Sum of post-translation skb->len
} ClatEgress4Value;
-STRUCT_SIZE(ClatEgress4Value, 4 + 2 * 16 + 1 + 3); // 40
+STRUCT_SIZE(ClatEgress4Value, 4 + 2 * 16 + 1 + 3 + 8 + 8); // 56
#undef STRUCT_SIZE
diff --git a/bpf_progs/dscpPolicy.c b/bpf_progs/dscpPolicy.c
index e845a69..ed114e4 100644
--- a/bpf_progs/dscpPolicy.c
+++ b/bpf_progs/dscpPolicy.c
@@ -27,8 +27,8 @@
#include <stdint.h>
#include <string.h>
-// The resulting .o needs to load on the Android T bpfloader
-#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
+// The resulting .o needs to load on Android T+
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
#include "bpf_helpers.h"
#include "dscpPolicy.h"
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index f223dd1..dfc7699 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-// The resulting .o needs to load on the Android T bpfloader
-#define BPFLOADER_MIN_VER BPFLOADER_T_VERSION
+// The resulting .o needs to load on Android T+
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
#include <bpf_helpers.h>
#include <linux/bpf.h>
@@ -103,13 +103,13 @@
// A single-element configuration array, packet tracing is enabled when 'true'.
DEFINE_BPF_MAP_EXT(packet_trace_enabled_map, ARRAY, uint32_t, bool, 1,
AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", PRIVATE,
- BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
LOAD_ON_USER, LOAD_ON_USERDEBUG)
// A ring buffer on which packet information is pushed.
DEFINE_BPF_RINGBUF_EXT(packet_trace_ringbuf, PacketTrace, PACKET_TRACE_BUF_SIZE,
AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", PRIVATE,
- BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
LOAD_ON_USER, LOAD_ON_USERDEBUG);
DEFINE_BPF_MAP_RO_NETD(data_saver_enabled_map, ARRAY, uint32_t, bool,
@@ -446,8 +446,18 @@
const struct egress_bool egress,
const bool enable_tracing,
const struct kver_uint kver) {
+ // sock_uid will be 'overflowuid' if !sk_fullsock(sk_to_full_sk(skb->sk))
uint32_t sock_uid = bpf_get_socket_uid(skb);
- uint64_t cookie = bpf_get_socket_cookie(skb);
+
+ // kernel's DEFAULT_OVERFLOWUID is 65534, this is the overflow 'nobody' uid,
+ // usually this being returned means that skb->sk is NULL during RX
+ // (early decap socket lookup failure), which commonly happens for incoming
+ // packets to an unconnected udp socket.
+ // But it can also happen for egress from a timewait socket.
+ // Let's treat such cases as 'root' which is_system_uid()
+ if (sock_uid == 65534) sock_uid = 0;
+
+ uint64_t cookie = bpf_get_socket_cookie(skb); // 0 iff !skb->sk
UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
uint32_t uid, tag;
if (utag) {
@@ -506,7 +516,7 @@
// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace_user", AID_ROOT, AID_SYSTEM,
bpf_cgroup_ingress_trace_user, KVER_5_8, KVER_INF,
- BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
"fs_bpf_netd_readonly", "",
IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
(struct __sk_buff* skb) {
@@ -516,7 +526,7 @@
// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
bpf_cgroup_ingress_trace, KVER_5_8, KVER_INF,
- BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, MANDATORY,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
"fs_bpf_netd_readonly", "",
LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
(struct __sk_buff* skb) {
@@ -538,9 +548,9 @@
// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace_user", AID_ROOT, AID_SYSTEM,
bpf_cgroup_egress_trace_user, KVER_5_8, KVER_INF,
- BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
"fs_bpf_netd_readonly", "",
- LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
+ IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
}
@@ -548,7 +558,7 @@
// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
bpf_cgroup_egress_trace, KVER_5_8, KVER_INF,
- BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, MANDATORY,
+ BPFLOADER_MAINLINE_U_VERSION, BPFLOADER_MAX_VER, MANDATORY,
"fs_bpf_netd_readonly", "",
LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
(struct __sk_buff* skb) {
@@ -616,12 +626,13 @@
uint32_t sock_uid = bpf_get_socket_uid(skb);
if (is_system_uid(sock_uid)) return BPF_MATCH;
- // 65534 is the overflow 'nobody' uid, usually this being returned means
- // that skb->sk is NULL during RX (early decap socket lookup failure),
- // which commonly happens for incoming packets to an unconnected udp socket.
- // Additionally bpf_get_socket_cookie() returns 0 if skb->sk is NULL
- if ((sock_uid == 65534) && !bpf_get_socket_cookie(skb) && is_received_skb(skb))
- return BPF_MATCH;
+ // kernel's DEFAULT_OVERFLOWUID is 65534, this is the overflow 'nobody' uid,
+ // usually this being returned means that skb->sk is NULL during RX
+ // (early decap socket lookup failure), which commonly happens for incoming
+ // packets to an unconnected udp socket.
+ // But it can also happen for egress from a timewait socket.
+ // Let's treat such cases as 'root' which is_system_uid()
+ if (sock_uid == 65534) return BPF_MATCH;
UidOwnerValue* allowlistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
if (allowlistMatch) return allowlistMatch->rule & HAPPY_BOX_MATCH ? BPF_MATCH : BPF_NOMATCH;
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index 64ed633..098147f 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -178,7 +178,7 @@
#endif // __cplusplus
// LINT.IfChange(match_type)
-enum UidOwnerMatchType {
+enum UidOwnerMatchType : uint32_t {
NO_MATCH = 0,
HAPPY_BOX_MATCH = (1 << 0),
PENALTY_BOX_MATCH = (1 << 1),
@@ -196,14 +196,14 @@
};
// LINT.ThenChange(../framework/src/android/net/BpfNetMapsConstants.java)
-enum BpfPermissionMatch {
+enum BpfPermissionMatch : uint8_t {
BPF_PERMISSION_INTERNET = 1 << 2,
BPF_PERMISSION_UPDATE_DEVICE_STATS = 1 << 3,
};
// In production we use two identical stats maps to record per uid stats and
// do swap and clean based on the configuration specified here. The statsMapType
// value in configuration map specified which map is currently in use.
-enum StatsMapType {
+enum StatsMapType : uint32_t {
SELECT_MAP_A,
SELECT_MAP_B,
};
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index 90f96a1..4f152bf 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -24,16 +24,16 @@
#define __kernel_udphdr udphdr
#include <linux/udp.h>
-#ifdef BTF
+#ifdef MAINLINE
// BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
// ship a different file than for later versions, but we need bpfloader v0.25+
// for obj@ver.o support
-#define BPFLOADER_MIN_VER BPFLOADER_OBJ_AT_VER_VERSION
-#else /* BTF */
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
+#else /* MAINLINE */
// The resulting .o needs to load on the Android S bpfloader
#define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
-#define BPFLOADER_MAX_VER BPFLOADER_OBJ_AT_VER_VERSION
-#endif /* BTF */
+#define BPFLOADER_MAX_VER BPFLOADER_T_VERSION
+#endif /* MAINLINE */
// Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
#define TETHERING_UID AID_ROOT
diff --git a/bpf_progs/offload@btf.c b/bpf_progs/offload@mainline.c
similarity index 100%
rename from bpf_progs/offload@btf.c
rename to bpf_progs/offload@mainline.c
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index 70b08b7..fff3512 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -18,16 +18,16 @@
#include <linux/in.h>
#include <linux/ip.h>
-#ifdef BTF
+#ifdef MAINLINE
// BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
// ship a different file than for later versions, but we need bpfloader v0.25+
// for obj@ver.o support
-#define BPFLOADER_MIN_VER BPFLOADER_OBJ_AT_VER_VERSION
-#else /* BTF */
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
+#else /* MAINLINE */
// The resulting .o needs to load on the Android S bpfloader
#define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
-#define BPFLOADER_MAX_VER BPFLOADER_OBJ_AT_VER_VERSION
-#endif /* BTF */
+#define BPFLOADER_MAX_VER BPFLOADER_T_VERSION
+#endif /* MAINLINE */
// Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
#define TETHERING_UID AID_ROOT
diff --git a/bpf_progs/test@btf.c b/bpf_progs/test@mainline.c
similarity index 100%
rename from bpf_progs/test@btf.c
rename to bpf_progs/test@mainline.c
diff --git a/clatd/Android.bp b/clatd/Android.bp
index 595c6b9..43eb2d8 100644
--- a/clatd/Android.bp
+++ b/clatd/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["external_android-clat_license"],
}
@@ -54,7 +55,7 @@
defaults: ["clatd_defaults"],
srcs: [
":clatd_common",
- "main.c"
+ "main.c",
],
static_libs: [
"libip_checksum",
@@ -101,7 +102,7 @@
defaults: ["clatd_defaults"],
srcs: [
":clatd_common",
- "clatd_test.cpp"
+ "clatd_test.cpp",
],
static_libs: [
"libbase",
diff --git a/common/Android.bp b/common/Android.bp
index f2f3929..5fab146 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -15,11 +15,16 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
-// The library requires the final artifact to contain net-utils-device-common-struct.
+// This is a placeholder comment to avoid merge conflicts
+// as the above target may not exist
+// depending on the branch
+
+// The library requires the final artifact to contain net-utils-device-common-struct-base.
java_library {
name: "connectivity-net-module-utils-bpf",
srcs: [
@@ -38,10 +43,12 @@
// For libraries which are statically linked in framework-connectivity, do not
// statically link here because callers of this library might already have a static
// version linked.
- "net-utils-device-common-struct",
+ "net-utils-device-common-struct-base",
],
apex_available: [
"com.android.tethering",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
}
diff --git a/common/FlaggedApi.bp b/common/FlaggedApi.bp
new file mode 100644
index 0000000..56625c5
--- /dev/null
+++ b/common/FlaggedApi.bp
@@ -0,0 +1,39 @@
+//
+// 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.
+//
+
+aconfig_declarations {
+ name: "com.android.net.flags-aconfig",
+ package: "com.android.net.flags",
+ container: "system",
+ srcs: ["flags.aconfig"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+aconfig_declarations {
+ name: "com.android.net.thread.flags-aconfig",
+ package: "com.android.net.thread.flags",
+ container: "system",
+ srcs: ["thread_flags.aconfig"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+aconfig_declarations {
+ name: "nearby_flags",
+ package: "com.android.nearby.flags",
+ container: "system",
+ srcs: ["nearby_flags.aconfig"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
diff --git a/common/OWNERS b/common/OWNERS
new file mode 100644
index 0000000..e7f5d11
--- /dev/null
+++ b/common/OWNERS
@@ -0,0 +1 @@
+per-file thread_flags.aconfig = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/common/flags.aconfig b/common/flags.aconfig
new file mode 100644
index 0000000..30931df
--- /dev/null
+++ b/common/flags.aconfig
@@ -0,0 +1,85 @@
+package: "com.android.net.flags"
+container: "system"
+
+# This file contains aconfig flags for FlaggedAPI annotations
+# Flags used from platform code must be in under frameworks
+
+flag {
+ name: "set_data_saver_via_cm"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Set data saver through ConnectivityManager API"
+ bug: "297836825"
+}
+
+flag {
+ name: "support_is_uid_networking_blocked"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "This flag controls whether isUidNetworkingBlocked is supported"
+ bug: "297836825"
+}
+
+flag {
+ name: "basic_background_restrictions_enabled"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Block network access for apps in a low importance background state"
+ bug: "304347838"
+}
+
+flag {
+ name: "ipsec_transform_state"
+ is_exported: true
+ namespace: "android_core_networking_ipsec"
+ description: "The flag controls the access for getIpSecTransformState and IpSecTransformState"
+ bug: "308011229"
+}
+
+flag {
+ name: "tethering_request_with_soft_ap_config"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "The flag controls the access for the parcelable TetheringRequest with getSoftApConfiguration/setSoftApConfiguration API"
+ bug: "216524590"
+}
+
+flag {
+ name: "request_restricted_wifi"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Flag for API to support requesting restricted wifi"
+ bug: "315835605"
+}
+
+flag {
+ name: "net_capability_local_network"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Flag for local network capability API"
+ bug: "313000440"
+}
+
+flag {
+ name: "support_transport_satellite"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Flag for satellite transport API"
+ bug: "320514105"
+}
+
+flag {
+ name: "nsd_subtypes_support_enabled"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Flag for API to support nsd subtypes"
+ bug: "265095929"
+}
+
+flag {
+ name: "register_nsd_offload_engine_api"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Flag for API to register nsd offload engine"
+ bug: "301713539"
+}
diff --git a/common/nearby_flags.aconfig b/common/nearby_flags.aconfig
new file mode 100644
index 0000000..b733849
--- /dev/null
+++ b/common/nearby_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.nearby.flags"
+container: "system"
+
+flag {
+ name: "powered_off_finding"
+ is_exported: true
+ namespace: "nearby"
+ description: "Controls whether the Powered Off Finding feature is enabled"
+ bug: "307898240"
+}
diff --git a/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java b/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java
index 69fab09..71f7516 100644
--- a/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java
+++ b/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java
@@ -36,11 +36,24 @@
@Field(order = 3, type = Type.U8, padding = 3)
public final short oifIsEthernet; // Whether the output interface requires ethernet header
+ @Field(order = 4, type = Type.U63)
+ public final long packets; // Count of translated gso (large) packets
+
+ @Field(order = 5, type = Type.U63)
+ public final long bytes; // Sum of post-translation skb->len
+
public ClatEgress4Value(final int oif, final Inet6Address local6, final Inet6Address pfx96,
- final short oifIsEthernet) {
+ final short oifIsEthernet, final long packets, final long bytes) {
this.oif = oif;
this.local6 = local6;
this.pfx96 = pfx96;
this.oifIsEthernet = oifIsEthernet;
+ this.packets = packets;
+ this.bytes = bytes;
+ }
+
+ public ClatEgress4Value(final int oif, final Inet6Address local6, final Inet6Address pfx96,
+ final short oifIsEthernet) {
+ this(oif, local6, pfx96, oifIsEthernet, 0, 0);
}
}
diff --git a/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java b/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java
index fb81caa..25f737b 100644
--- a/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java
+++ b/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java
@@ -30,8 +30,21 @@
@Field(order = 1, type = Type.Ipv4Address)
public final Inet4Address local4; // The destination IPv4 address
- public ClatIngress6Value(final int oif, final Inet4Address local4) {
+ @Field(order = 2, type = Type.U63)
+ public final long packets; // Count of translated gso (large) packets
+
+ @Field(order = 3, type = Type.U63)
+ public final long bytes; // Sum of post-translation skb->len
+
+ public ClatIngress6Value(final int oif, final Inet4Address local4, final long packets,
+ final long bytes) {
this.oif = oif;
this.local4 = local4;
+ this.packets = packets;
+ this.bytes = bytes;
+ }
+
+ public ClatIngress6Value(final int oif, final Inet4Address local4) {
+ this(oif, local4, 0, 0);
}
}
diff --git a/thread/flags/thread_base.aconfig b/common/thread_flags.aconfig
similarity index 82%
rename from thread/flags/thread_base.aconfig
rename to common/thread_flags.aconfig
index bf1f288..43fc147 100644
--- a/thread/flags/thread_base.aconfig
+++ b/common/thread_flags.aconfig
@@ -1,7 +1,9 @@
package: "com.android.net.thread.flags"
+container: "system"
flag {
name: "thread_enabled"
+ is_exported: true
namespace: "thread_network"
description: "Controls whether the Android Thread feature is enabled"
bug: "301473012"
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 7d2c563..0ee2275 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -34,6 +35,7 @@
name: "enable-framework-connectivity-t-targets",
enabled: true,
}
+
// The above defaults can be used to disable framework-connectivity t
// targets while minimizing merge conflicts in the build rules.
@@ -116,6 +118,7 @@
"framework-bluetooth",
"framework-wifi",
"framework-connectivity-pre-jarjar",
+ "framework-location.stubs.module_lib",
],
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
@@ -138,6 +141,7 @@
"sdk_module-lib_current_framework-connectivity",
],
libs: [
+ "framework-location.stubs.module_lib",
"sdk_module-lib_current_framework-connectivity",
],
permitted_packages: [
@@ -178,6 +182,7 @@
"//frameworks/base/core/tests/bandwidthtests",
"//frameworks/base/core/tests/benchmarks",
"//frameworks/base/core/tests/utillib",
+ "//frameworks/base/services/tests/VpnTests",
"//frameworks/base/tests/vcn",
"//frameworks/opt/net/ethernet/tests:__subpackages__",
"//frameworks/opt/telephony/tests/telephonytests",
@@ -191,6 +196,11 @@
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
],
+ aconfig_declarations: [
+ "com.android.net.flags-aconfig",
+ "com.android.net.thread.flags-aconfig",
+ "nearby_flags",
+ ],
}
// This rule is not used anymore(b/268440216).
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index 60a88c0..9ae0cf7 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -127,7 +127,7 @@
public final class IpSecTransform implements java.lang.AutoCloseable {
method public void close();
- method @FlaggedApi("com.android.net.flags.ipsec_transform_state") public void getIpSecTransformState(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.IpSecTransformState,java.lang.RuntimeException>);
+ method @FlaggedApi("com.android.net.flags.ipsec_transform_state") public void requestIpSecTransformState(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.IpSecTransformState,java.lang.RuntimeException>);
}
public static class IpSecTransform.Builder {
@@ -145,7 +145,7 @@
method public long getPacketCount();
method @NonNull public byte[] getReplayBitmap();
method public long getRxHighestSequenceNumber();
- method public long getTimestamp();
+ method public long getTimestampMillis();
method public long getTxHighestSequenceNumber();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.IpSecTransformState> CREATOR;
@@ -158,7 +158,7 @@
method @NonNull public android.net.IpSecTransformState.Builder setPacketCount(long);
method @NonNull public android.net.IpSecTransformState.Builder setReplayBitmap(@NonNull byte[]);
method @NonNull public android.net.IpSecTransformState.Builder setRxHighestSequenceNumber(long);
- method @NonNull public android.net.IpSecTransformState.Builder setTimestamp(long);
+ method @NonNull public android.net.IpSecTransformState.Builder setTimestampMillis(long);
method @NonNull public android.net.IpSecTransformState.Builder setTxHighestSequenceNumber(long);
}
@@ -210,9 +210,26 @@
package android.net.nsd {
+ @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public final class DiscoveryRequest implements android.os.Parcelable {
+ method public int describeContents();
+ method @Nullable public android.net.Network getNetwork();
+ method @NonNull public String getServiceType();
+ method @Nullable public String getSubtype();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.DiscoveryRequest> CREATOR;
+ }
+
+ public static final class DiscoveryRequest.Builder {
+ ctor public DiscoveryRequest.Builder(@NonNull String);
+ method @NonNull public android.net.nsd.DiscoveryRequest build();
+ method @NonNull public android.net.nsd.DiscoveryRequest.Builder setNetwork(@Nullable android.net.Network);
+ method @NonNull public android.net.nsd.DiscoveryRequest.Builder setSubtype(@Nullable String);
+ }
+
public final class NsdManager {
method public void discoverServices(String, int, android.net.nsd.NsdManager.DiscoveryListener);
method public void discoverServices(@NonNull String, int, @Nullable android.net.Network, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
+ method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public void discoverServices(@NonNull android.net.nsd.DiscoveryRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void discoverServices(@NonNull String, int, @NonNull android.net.NetworkRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
method public void registerService(android.net.nsd.NsdServiceInfo, int, android.net.nsd.NsdManager.RegistrationListener);
method public void registerService(@NonNull android.net.nsd.NsdServiceInfo, int, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.RegistrationListener);
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 3513573..1f1953c 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -59,11 +59,17 @@
}
public class NearbyManager {
+ method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public int getPoweredOffFindingMode();
method public void queryOffloadCapability(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.nearby.OffloadCapability>);
+ method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void setPoweredOffFindingEphemeralIds(@NonNull java.util.List<byte[]>);
+ method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void setPoweredOffFindingMode(int);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void startBroadcast(@NonNull android.nearby.BroadcastRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.BroadcastCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int startScan(@NonNull android.nearby.ScanRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.ScanCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopBroadcast(@NonNull android.nearby.BroadcastCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopScan(@NonNull android.nearby.ScanCallback);
+ field @FlaggedApi("com.android.nearby.flags.powered_off_finding") public static final int POWERED_OFF_FINDING_MODE_DISABLED = 1; // 0x1
+ field @FlaggedApi("com.android.nearby.flags.powered_off_finding") public static final int POWERED_OFF_FINDING_MODE_ENABLED = 2; // 0x2
+ field @FlaggedApi("com.android.nearby.flags.powered_off_finding") public static final int POWERED_OFF_FINDING_MODE_UNSUPPORTED = 0; // 0x0
}
public final class OffloadCapability implements android.os.Parcelable {
@@ -500,6 +506,7 @@
method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void registerOperationalDatasetCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.StateCallback);
method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void scheduleMigration(@NonNull android.net.thread.PendingOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void setEnabled(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void unregisterStateCallback(@NonNull android.net.thread.ThreadNetworkController.StateCallback);
field public static final int DEVICE_ROLE_CHILD = 2; // 0x2
@@ -507,6 +514,9 @@
field public static final int DEVICE_ROLE_LEADER = 4; // 0x4
field public static final int DEVICE_ROLE_ROUTER = 3; // 0x3
field public static final int DEVICE_ROLE_STOPPED = 0; // 0x0
+ field public static final int STATE_DISABLED = 0; // 0x0
+ field public static final int STATE_DISABLING = 2; // 0x2
+ field public static final int STATE_ENABLED = 1; // 0x1
field public static final int THREAD_VERSION_1_3 = 4; // 0x4
}
@@ -518,6 +528,7 @@
public static interface ThreadNetworkController.StateCallback {
method public void onDeviceRoleChanged(int);
method public default void onPartitionIdChanged(long);
+ method public default void onThreadEnableStateChanged(int);
}
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public class ThreadNetworkException extends java.lang.Exception {
@@ -530,8 +541,10 @@
field public static final int ERROR_REJECTED_BY_PEER = 8; // 0x8
field public static final int ERROR_RESOURCE_EXHAUSTED = 10; // 0xa
field public static final int ERROR_RESPONSE_BAD_FORMAT = 9; // 0x9
+ field public static final int ERROR_THREAD_DISABLED = 12; // 0xc
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/app/usage/NetworkStatsManager.java b/framework-t/src/android/app/usage/NetworkStatsManager.java
index d139544..18c839f 100644
--- a/framework-t/src/android/app/usage/NetworkStatsManager.java
+++ b/framework-t/src/android/app/usage/NetworkStatsManager.java
@@ -510,6 +510,27 @@
* Query network usage statistics details for a given uid.
* This may take a long time, and apps should avoid calling this on their main thread.
*
+ * @param networkType As defined in {@link ConnectivityManager}, e.g.
+ * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+ * etc.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * <p>Starting with API level 29, the {@code subscriberId} is guarded by
+ * additional restrictions. Calling apps that do not meet the new
+ * requirements to access the {@code subscriberId} can provide a {@code
+ * null} value when querying for the mobile network type to receive usage
+ * for all mobile networks. For additional details see {@link
+ * TelephonyManager#getSubscriberId()}.
+ * <p>Starting with API level 31, calling apps can provide a
+ * {@code subscriberId} with wifi network type to receive usage for
+ * wifi networks which is under the given subscription if applicable.
+ * Otherwise, pass {@code null} when querying all wifi networks.
+ * @param startTime Start of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param uid UID of app
+ * @return Statistics which is described above.
+ * @throws SecurityException if permissions are insufficient to read network statistics.
* @see #queryDetailsForUidTagState(int, String, long, long, int, int, int)
*/
@NonNull
@@ -731,8 +752,8 @@
/**
* Query realtime mobile network usage statistics.
*
- * Return a snapshot of current UID network statistics, as it applies
- * to the mobile radios of the device. The snapshot will include any
+ * Return a snapshot of current UID network statistics for both cellular and satellite (which
+ * also uses same mobile radio as cellular) when called. The snapshot will include any
* tethering traffic, video calling data usage and count of
* network operations set by {@link TrafficStats#incrementOperationCount}
* made over a mobile radio.
diff --git a/framework-t/src/android/net/EthernetManager.java b/framework-t/src/android/net/EthernetManager.java
index b8070f0..719f60d 100644
--- a/framework-t/src/android/net/EthernetManager.java
+++ b/framework-t/src/android/net/EthernetManager.java
@@ -642,7 +642,14 @@
}
/**
- * Listen to changes in the state of ethernet.
+ * Register a IntConsumer to be called back on ethernet state changes.
+ *
+ * <p>{@link IntConsumer#accept} with the current ethernet state will be triggered immediately
+ * upon adding a listener. The same callback is invoked on Ethernet state change, i.e. when
+ * calling {@link #setEthernetEnabled}.
+ * <p>The reported state is represented by:
+ * {@link #ETHERNET_STATE_DISABLED}: ethernet is now disabled.
+ * {@link #ETHERNET_STATE_ENABLED}: ethernet is now enabled.
*
* @param executor to run callbacks on.
* @param listener to listen ethernet state changed.
diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java
index 246a2dd..4e10a96 100644
--- a/framework-t/src/android/net/IpSecTransform.java
+++ b/framework-t/src/android/net/IpSecTransform.java
@@ -215,7 +215,7 @@
* @see IpSecTransformState
*/
@FlaggedApi(IPSEC_TRANSFORM_STATE)
- public void getIpSecTransformState(
+ public void requestIpSecTransformState(
@CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<IpSecTransformState, RuntimeException> callback) {
Objects.requireNonNull(executor);
diff --git a/framework-t/src/android/net/IpSecTransformState.java b/framework-t/src/android/net/IpSecTransformState.java
index b575dd5..5b80ae2 100644
--- a/framework-t/src/android/net/IpSecTransformState.java
+++ b/framework-t/src/android/net/IpSecTransformState.java
@@ -23,6 +23,7 @@
import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.SystemClock;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.HexDump;
@@ -40,7 +41,7 @@
*/
@FlaggedApi(IPSEC_TRANSFORM_STATE)
public final class IpSecTransformState implements Parcelable {
- private final long mTimeStamp;
+ private final long mTimestamp;
private final long mTxHighestSequenceNumber;
private final long mRxHighestSequenceNumber;
private final long mPacketCount;
@@ -54,7 +55,7 @@
long packetCount,
long byteCount,
byte[] replayBitmap) {
- mTimeStamp = timestamp;
+ mTimestamp = timestamp;
mTxHighestSequenceNumber = txHighestSequenceNumber;
mRxHighestSequenceNumber = rxHighestSequenceNumber;
mPacketCount = packetCount;
@@ -78,7 +79,7 @@
@VisibleForTesting(visibility = Visibility.PRIVATE)
public IpSecTransformState(@NonNull Parcel in) {
Objects.requireNonNull(in, "The input PersistableBundle is null");
- mTimeStamp = in.readLong();
+ mTimestamp = in.readLong();
mTxHighestSequenceNumber = in.readLong();
mRxHighestSequenceNumber = in.readLong();
mPacketCount = in.readLong();
@@ -97,7 +98,7 @@
@Override
public void writeToParcel(@NonNull Parcel out, int flags) {
- out.writeLong(mTimeStamp);
+ out.writeLong(mTimestamp);
out.writeLong(mTxHighestSequenceNumber);
out.writeLong(mRxHighestSequenceNumber);
out.writeLong(mPacketCount);
@@ -120,16 +121,17 @@
};
/**
- * Retrieve the epoch timestamp (milliseconds) for when this state was created
+ * Retrieve the timestamp (milliseconds) when this state was created, as per {@link
+ * SystemClock#elapsedRealtime}
*
- * @see Builder#setTimestamp(long)
+ * @see Builder#setTimestampMillis(long)
*/
- public long getTimestamp() {
- return mTimeStamp;
+ public long getTimestampMillis() {
+ return mTimestamp;
}
/**
- * Retrieve the highest sequence number sent so far
+ * Retrieve the highest sequence number sent so far as an unsigned long
*
* @see Builder#setTxHighestSequenceNumber(long)
*/
@@ -138,7 +140,7 @@
}
/**
- * Retrieve the highest sequence number received so far
+ * Retrieve the highest sequence number received so far as an unsigned long
*
* @see Builder#setRxHighestSequenceNumber(long)
*/
@@ -147,7 +149,10 @@
}
/**
- * Retrieve the number of packets received AND sent so far
+ * Retrieve the number of packets processed so far as an unsigned long.
+ *
+ * <p>The packet count direction (inbound or outbound) aligns with the direction in which the
+ * IpSecTransform is applied to.
*
* @see Builder#setPacketCount(long)
*/
@@ -156,7 +161,10 @@
}
/**
- * Retrieve the number of bytes received AND sent so far
+ * Retrieve the number of bytes processed so far as an unsigned long
+ *
+ * <p>The byte count direction (inbound or outbound) aligns with the direction in which the
+ * IpSecTransform is applied to.
*
* @see Builder#setByteCount(long)
*/
@@ -183,10 +191,15 @@
return mReplayBitmap.clone();
}
- /** Builder class for testing purposes */
+ /**
+ * Builder class for testing purposes
+ *
+ * <p>Except for testing, IPsec callers normally do not instantiate {@link IpSecTransformState}
+ * themselves but instead get a reference via {@link IpSecTransformState}
+ */
@FlaggedApi(IPSEC_TRANSFORM_STATE)
public static final class Builder {
- private long mTimeStamp;
+ private long mTimestamp;
private long mTxHighestSequenceNumber;
private long mRxHighestSequenceNumber;
private long mPacketCount;
@@ -194,22 +207,22 @@
private byte[] mReplayBitmap;
public Builder() {
- mTimeStamp = System.currentTimeMillis();
+ mTimestamp = SystemClock.elapsedRealtime();
}
/**
- * Set the epoch timestamp (milliseconds) for when this state was created
+ * Set the timestamp (milliseconds) when this state was created
*
- * @see IpSecTransformState#getTimestamp()
+ * @see IpSecTransformState#getTimestampMillis()
*/
@NonNull
- public Builder setTimestamp(long timeStamp) {
- mTimeStamp = timeStamp;
+ public Builder setTimestampMillis(long timestamp) {
+ mTimestamp = timestamp;
return this;
}
/**
- * Set the highest sequence number sent so far
+ * Set the highest sequence number sent so far as an unsigned long
*
* @see IpSecTransformState#getTxHighestSequenceNumber()
*/
@@ -220,7 +233,7 @@
}
/**
- * Set the highest sequence number received so far
+ * Set the highest sequence number received so far as an unsigned long
*
* @see IpSecTransformState#getRxHighestSequenceNumber()
*/
@@ -231,7 +244,7 @@
}
/**
- * Set the number of packets received AND sent so far
+ * Set the number of packets processed so far as an unsigned long
*
* @see IpSecTransformState#getPacketCount()
*/
@@ -242,7 +255,7 @@
}
/**
- * Set the number of bytes received AND sent so far
+ * Set the number of bytes processed so far as an unsigned long
*
* @see IpSecTransformState#getByteCount()
*/
@@ -271,7 +284,7 @@
@NonNull
public IpSecTransformState build() {
return new IpSecTransformState(
- mTimeStamp,
+ mTimestamp,
mTxHighestSequenceNumber,
mRxHighestSequenceNumber,
mPacketCount,
diff --git a/framework-t/src/android/net/NetworkStatsAccess.java b/framework-t/src/android/net/NetworkStatsAccess.java
index 23902dc..7c9b3ec 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);
@@ -127,7 +128,7 @@
final int appId = UserHandle.getAppId(callingUid);
- final boolean isNetworkStack = PermissionUtils.checkAnyPermissionOf(
+ final boolean isNetworkStack = PermissionUtils.hasAnyPermissionOf(
context, callingPid, callingUid, android.Manifest.permission.NETWORK_STACK,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
index b1ef98f..6afb2d5 100644
--- a/framework-t/src/android/net/nsd/AdvertisingRequest.java
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -17,11 +17,13 @@
import android.annotation.LongDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.time.Duration;
import java.util.Objects;
/**
@@ -34,7 +36,7 @@
/**
* Only update the registration without sending exit and re-announcement.
*/
- public static final int NSD_ADVERTISING_UPDATE_ONLY = 1;
+ public static final long NSD_ADVERTISING_UPDATE_ONLY = 1;
@NonNull
@@ -46,7 +48,9 @@
NsdServiceInfo.class.getClassLoader(), NsdServiceInfo.class);
final int protocolType = in.readInt();
final long advertiseConfig = in.readLong();
- return new AdvertisingRequest(serviceInfo, protocolType, advertiseConfig);
+ final long ttlSeconds = in.readLong();
+ final Duration ttl = ttlSeconds < 0 ? null : Duration.ofSeconds(ttlSeconds);
+ return new AdvertisingRequest(serviceInfo, protocolType, advertiseConfig, ttl);
}
@Override
@@ -60,6 +64,9 @@
// Bitmask of @AdvertisingConfig flags. Uses a long to allow 64 possible flags in the future.
private final long mAdvertisingConfig;
+ @Nullable
+ private final Duration mTtl;
+
/**
* @hide
*/
@@ -73,10 +80,11 @@
* The constructor for the advertiseRequest
*/
private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, int protocolType,
- long advertisingConfig) {
+ long advertisingConfig, @NonNull Duration ttl) {
mServiceInfo = serviceInfo;
mProtocolType = protocolType;
mAdvertisingConfig = advertisingConfig;
+ mTtl = ttl;
}
/**
@@ -101,12 +109,26 @@
return mAdvertisingConfig;
}
+ /**
+ * Returns the time interval that the resource records may be cached on a DNS resolver.
+ *
+ * The value will be {@code null} if it's not specified with the {@link #Builder}.
+ *
+ * @hide
+ */
+ // @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_TTL_ENABLED)
+ @Nullable
+ public Duration getTtl() {
+ return mTtl;
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("serviceInfo: ").append(mServiceInfo)
.append(", protocolType: ").append(mProtocolType)
- .append(", advertisingConfig: ").append(mAdvertisingConfig);
+ .append(", advertisingConfig: ").append(mAdvertisingConfig)
+ .append(", ttl: ").append(mTtl);
return sb.toString();
}
@@ -120,13 +142,14 @@
final AdvertisingRequest otherRequest = (AdvertisingRequest) other;
return mServiceInfo.equals(otherRequest.mServiceInfo)
&& mProtocolType == otherRequest.mProtocolType
- && mAdvertisingConfig == otherRequest.mAdvertisingConfig;
+ && mAdvertisingConfig == otherRequest.mAdvertisingConfig
+ && Objects.equals(mTtl, otherRequest.mTtl);
}
}
@Override
public int hashCode() {
- return Objects.hash(mServiceInfo, mProtocolType, mAdvertisingConfig);
+ return Objects.hash(mServiceInfo, mProtocolType, mAdvertisingConfig, mTtl);
}
@Override
@@ -139,6 +162,7 @@
dest.writeParcelable(mServiceInfo, flags);
dest.writeInt(mProtocolType);
dest.writeLong(mAdvertisingConfig);
+ dest.writeLong(mTtl == null ? -1L : mTtl.getSeconds());
}
// @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
@@ -151,6 +175,8 @@
private final NsdServiceInfo mServiceInfo;
private final int mProtocolType;
private long mAdvertisingConfig;
+ @Nullable
+ private Duration mTtl;
/**
* Creates a new {@link Builder} object.
*/
@@ -170,11 +196,46 @@
return this;
}
+ /**
+ * Sets the time interval that the resource records may be cached on a DNS resolver.
+ *
+ * If this method is not called or {@code ttl} is {@code null}, default TTL values
+ * will be used for the service when it's registered. Otherwise, the {@code ttl}
+ * will be used for all resource records of this service.
+ *
+ * When registering a service, {@link NsdManager#FAILURE_BAD_PARAMETERS} will be returned
+ * if {@code ttl} is smaller than 30 seconds.
+ *
+ * Note: the value after the decimal point (in unit of seconds) will be discarded. For
+ * example, {@code 30} seconds will be used when {@code Duration.ofSeconds(30L, 50_000L)}
+ * is provided.
+ *
+ * @param ttl the maximum duration that the DNS resource records will be cached
+ *
+ * @see AdvertisingRequest#getTtl
+ * @hide
+ */
+ // @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_TTL_ENABLED)
+ @NonNull
+ public Builder setTtl(@Nullable Duration ttl) {
+ if (ttl == null) {
+ mTtl = null;
+ return this;
+ }
+ final long ttlSeconds = ttl.getSeconds();
+ if (ttlSeconds < 0 || ttlSeconds > 0xffffffffL) {
+ throw new IllegalArgumentException(
+ "ttlSeconds exceeds the allowed range (value = " + ttlSeconds
+ + ", allowedRanged = [0, 0xffffffffL])");
+ }
+ mTtl = Duration.ofSeconds(ttlSeconds);
+ return this;
+ }
/** Creates a new {@link AdvertisingRequest} object. */
@NonNull
public AdvertisingRequest build() {
- return new AdvertisingRequest(mServiceInfo, mProtocolType, mAdvertisingConfig);
+ return new AdvertisingRequest(mServiceInfo, mProtocolType, mAdvertisingConfig, mTtl);
}
}
}
diff --git a/framework-t/src/android/net/nsd/DiscoveryRequest.java b/framework-t/src/android/net/nsd/DiscoveryRequest.java
new file mode 100644
index 0000000..b0b71ea
--- /dev/null
+++ b/framework-t/src/android/net/nsd/DiscoveryRequest.java
@@ -0,0 +1,247 @@
+/*
+ * 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.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Network;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * Encapsulates parameters for {@link NsdManager#discoverServices}.
+ */
+@FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+public final class DiscoveryRequest implements Parcelable {
+ private final int mProtocolType;
+
+ @NonNull
+ private final String mServiceType;
+
+ @Nullable
+ private final String mSubtype;
+
+ @Nullable
+ private final Network mNetwork;
+
+ // TODO: add mDiscoveryConfig for more fine-grained discovery behavior control
+
+ @NonNull
+ public static final Creator<DiscoveryRequest> CREATOR =
+ new Creator<>() {
+ @Override
+ public DiscoveryRequest createFromParcel(Parcel in) {
+ int protocolType = in.readInt();
+ String serviceType = in.readString();
+ String subtype = in.readString();
+ Network network =
+ in.readParcelable(Network.class.getClassLoader(), Network.class);
+ return new DiscoveryRequest(protocolType, serviceType, subtype, network);
+ }
+
+ @Override
+ public DiscoveryRequest[] newArray(int size) {
+ return new DiscoveryRequest[size];
+ }
+ };
+
+ private DiscoveryRequest(int protocolType, @NonNull String serviceType,
+ @Nullable String subtype, @Nullable Network network) {
+ mProtocolType = protocolType;
+ mServiceType = serviceType;
+ mSubtype = subtype;
+ mNetwork = network;
+ }
+
+ /**
+ * Returns the service type in format of dot-joint string of two labels.
+ *
+ * For example, "_ipp._tcp" for internet printer and "_matter._tcp" for <a
+ * href="https://csa-iot.org/all-solutions/matter">Matter</a> operational device.
+ */
+ @NonNull
+ public String getServiceType() {
+ return mServiceType;
+ }
+
+ /**
+ * Returns the subtype without the trailing "._sub" label or {@code null} if no subtype is
+ * specified.
+ *
+ * For example, the return value will be "_printer" for subtype "_printer._sub".
+ */
+ @Nullable
+ public String getSubtype() {
+ return mSubtype;
+ }
+
+ /**
+ * Returns the service discovery protocol.
+ *
+ * @hide
+ */
+ public int getProtocolType() {
+ return mProtocolType;
+ }
+
+ /**
+ * Returns the {@link Network} on which the query should be sent or {@code null} if no
+ * network is specified.
+ */
+ @Nullable
+ public Network getNetwork() {
+ return mNetwork;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(", protocolType: ").append(mProtocolType)
+ .append(", serviceType: ").append(mServiceType)
+ .append(", subtype: ").append(mSubtype)
+ .append(", network: ").append(mNetwork);
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof DiscoveryRequest)) {
+ return false;
+ } else {
+ DiscoveryRequest otherRequest = (DiscoveryRequest) other;
+ return mProtocolType == otherRequest.mProtocolType
+ && Objects.equals(mServiceType, otherRequest.mServiceType)
+ && Objects.equals(mSubtype, otherRequest.mSubtype)
+ && Objects.equals(mNetwork, otherRequest.mNetwork);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mProtocolType, mServiceType, mSubtype, mNetwork);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mProtocolType);
+ dest.writeString(mServiceType);
+ dest.writeString(mSubtype);
+ dest.writeParcelable(mNetwork, flags);
+ }
+
+ /** The builder for creating new {@link DiscoveryRequest} objects. */
+ public static final class Builder {
+ private final int mProtocolType;
+
+ @NonNull
+ private String mServiceType;
+
+ @Nullable
+ private String mSubtype;
+
+ @Nullable
+ private Network mNetwork;
+
+ /**
+ * Creates a new default {@link Builder} object with given service type.
+ *
+ * @throws IllegalArgumentException if {@code serviceType} is {@code null} or an empty
+ * string
+ */
+ public Builder(@NonNull String serviceType) {
+ this(NsdManager.PROTOCOL_DNS_SD, serviceType);
+ }
+
+ /** @hide */
+ public Builder(int protocolType, @NonNull String serviceType) {
+ NsdManager.checkProtocol(protocolType);
+ mProtocolType = protocolType;
+ setServiceType(serviceType);
+ }
+
+ /**
+ * Sets the service type to be discovered or {@code null} if no services should be queried.
+ *
+ * The {@code serviceType} must be a dot-joint string of two labels. For example,
+ * "_ipp._tcp" for internet printer. Additionally, the first label must start with
+ * underscore ('_') and the second label must be either "_udp" or "_tcp". Otherwise, {@link
+ * NsdManager#discoverServices} will fail with {@link NsdManager#FAILURE_BAD_PARAMETER}.
+ *
+ * @throws IllegalArgumentException if {@code serviceType} is {@code null} or an empty
+ * string
+ *
+ * @hide
+ */
+ @NonNull
+ public Builder setServiceType(@NonNull String serviceType) {
+ if (TextUtils.isEmpty(serviceType)) {
+ throw new IllegalArgumentException("Service type cannot be empty");
+ }
+ mServiceType = serviceType;
+ return this;
+ }
+
+ /**
+ * Sets the optional subtype of the services to be discovered.
+ *
+ * If a non-empty {@code subtype} is specified, it must start with underscore ('_') and
+ * have the trailing "._sub" removed. Otherwise, {@link NsdManager#discoverServices} will
+ * fail with {@link NsdManager#FAILURE_BAD_PARAMETER}. For example, {@code subtype} should
+ * be "_printer" for DNS name "_printer._sub._http._tcp". In this case, only services with
+ * this {@code subtype} will be queried, rather than all services of the base service type.
+ *
+ * Note that a non-empty service type must be specified with {@link #setServiceType} if a
+ * non-empty subtype is specified by this method.
+ */
+ @NonNull
+ public Builder setSubtype(@Nullable String subtype) {
+ mSubtype = subtype;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Network} on which the discovery queries should be sent.
+ *
+ * @param network the discovery network or {@code null} if the query should be sent on
+ * all supported networks
+ */
+ @NonNull
+ public Builder setNetwork(@Nullable Network network) {
+ mNetwork = network;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link DiscoveryRequest} object.
+ */
+ @NonNull
+ public DiscoveryRequest build() {
+ return new DiscoveryRequest(mProtocolType, mServiceType, mSubtype, mNetwork);
+ }
+ }
+}
diff --git a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
index d89bfa9..55820ec 100644
--- a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
+++ b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
@@ -17,6 +17,7 @@
package android.net.nsd;
import android.os.Messenger;
+import android.net.nsd.DiscoveryRequest;
import android.net.nsd.NsdServiceInfo;
/**
@@ -24,7 +25,7 @@
* @hide
*/
oneway interface INsdManagerCallback {
- void onDiscoverServicesStarted(int listenerKey, in NsdServiceInfo info);
+ void onDiscoverServicesStarted(int listenerKey, in DiscoveryRequest discoveryRequest);
void onDiscoverServicesFailed(int listenerKey, int error);
void onServiceFound(int listenerKey, in NsdServiceInfo info);
void onServiceLost(int listenerKey, in NsdServiceInfo info);
diff --git a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
index b03eb29..9a31278 100644
--- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -17,6 +17,7 @@
package android.net.nsd;
import android.net.nsd.AdvertisingRequest;
+import android.net.nsd.DiscoveryRequest;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.IOffloadEngine;
import android.net.nsd.NsdServiceInfo;
@@ -30,7 +31,7 @@
interface INsdServiceConnector {
void registerService(int listenerKey, in AdvertisingRequest advertisingRequest);
void unregisterService(int listenerKey);
- void discoverServices(int listenerKey, in NsdServiceInfo serviceInfo);
+ void discoverServices(int listenerKey, in DiscoveryRequest discoveryRequest);
void stopDiscovery(int listenerKey);
void resolveService(int listenerKey, in NsdServiceInfo serviceInfo);
void startDaemon();
@@ -39,4 +40,4 @@
void unregisterServiceInfoCallback(int listenerKey);
void registerOffloadEngine(String ifaceName, in IOffloadEngine cb, long offloadCapabilities, long offloadType);
void unregisterOffloadEngine(in IOffloadEngine cb);
-}
\ No newline at end of file
+}
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index b4f2be9..1001423 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -158,13 +158,38 @@
"com.android.net.flags.nsd_subtypes_support_enabled";
static final String ADVERTISE_REQUEST_API =
"com.android.net.flags.advertise_request_api";
+ static final String NSD_CUSTOM_HOSTNAME_ENABLED =
+ "com.android.net.flags.nsd_custom_hostname_enabled";
+ static final String NSD_CUSTOM_TTL_ENABLED =
+ "com.android.net.flags.nsd_custom_ttl_enabled";
}
/**
* A regex for the acceptable format of a type or subtype label.
* @hide
*/
- public static final String TYPE_SUBTYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
+ public static final String TYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
+
+ /**
+ * A regex for the acceptable format of a subtype label.
+ *
+ * As per RFC 6763 7.1, "Subtype strings are not required to begin with an underscore, though
+ * they often do.", and "Subtype strings [...] may be constructed using arbitrary 8-bit data
+ * values. In many cases these data values may be UTF-8 [RFC3629] representations of text, or
+ * even (as in the example above) plain ASCII [RFC20], but they do not have to be.".
+ *
+ * This regex is overly conservative as it mandates the underscore and only allows printable
+ * ASCII characters (codes 0x20 to 0x7e, space to tilde), except for comma (0x2c) and dot
+ * (0x2e); so the NsdManager API does not allow everything the RFC allows. This may be revisited
+ * in the future, but using arbitrary bytes makes logging and testing harder, and using other
+ * characters would probably be a bad idea for interoperability for apps.
+ * @hide
+ */
+ public static final String SUBTYPE_LABEL_REGEX = "_["
+ + "\\x20-\\x2b"
+ + "\\x2d"
+ + "\\x2f-\\x7e"
+ + "]{1,62}";
/**
* A regex for the acceptable format of a service type specification.
@@ -177,14 +202,14 @@
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 + ")\\.)?"
+ "^(?:(" + SUBTYPE_LABEL_REGEX + ")\\.)?"
// Actual type (_type._tcp.local)
- + "(" + TYPE_SUBTYPE_LABEL_REGEX + "\\._(?:tcp|udp))"
+ + "(" + TYPE_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 + ")*)"
+ + "((?:," + SUBTYPE_LABEL_REGEX + ")*)"
+ "$";
/**
@@ -304,6 +329,20 @@
/** Dns based service discovery protocol */
public static final int PROTOCOL_DNS_SD = 0x0001;
+ /**
+ * The minimum TTL seconds which is allowed for a service registration.
+ *
+ * @hide
+ */
+ public static final long TTL_SECONDS_MIN = 30L;
+
+ /**
+ * The maximum TTL seconds which is allowed for a service registration.
+ *
+ * @hide
+ */
+ public static final long TTL_SECONDS_MAX = 10 * 3600L;
+
private static final SparseArray<String> EVENT_NAMES = new SparseArray<>();
static {
EVENT_NAMES.put(DISCOVER_SERVICES, "DISCOVER_SERVICES");
@@ -360,6 +399,8 @@
@GuardedBy("mMapLock")
private final SparseArray<NsdServiceInfo> mServiceMap = new SparseArray<>();
@GuardedBy("mMapLock")
+ private final SparseArray<DiscoveryRequest> mDiscoveryMap = new SparseArray<>();
+ @GuardedBy("mMapLock")
private final SparseArray<Executor> mExecutorMap = new SparseArray<>();
private final Object mMapLock = new Object();
// Map of listener key sent by client -> per-network discovery tracker
@@ -715,6 +756,12 @@
mServHandler.sendMessage(mServHandler.obtainMessage(message, 0, listenerKey, info));
}
+ private void sendDiscoveryRequest(
+ int message, int listenerKey, DiscoveryRequest discoveryRequest) {
+ mServHandler.sendMessage(
+ mServHandler.obtainMessage(message, 0, listenerKey, discoveryRequest));
+ }
+
private void sendError(int message, int listenerKey, int error) {
mServHandler.sendMessage(mServHandler.obtainMessage(message, error, listenerKey));
}
@@ -724,8 +771,8 @@
}
@Override
- public void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info) {
- sendInfo(DISCOVER_SERVICES_STARTED, listenerKey, info);
+ public void onDiscoverServicesStarted(int listenerKey, DiscoveryRequest discoveryRequest) {
+ sendDiscoveryRequest(DISCOVER_SERVICES_STARTED, listenerKey, discoveryRequest);
}
@Override
@@ -1003,10 +1050,12 @@
final Object obj = message.obj;
final Object listener;
final NsdServiceInfo ns;
+ final DiscoveryRequest discoveryRequest;
final Executor executor;
synchronized (mMapLock) {
listener = mListenerMap.get(key);
ns = mServiceMap.get(key);
+ discoveryRequest = mDiscoveryMap.get(key);
executor = mExecutorMap.get(key);
}
if (listener == null) {
@@ -1014,17 +1063,22 @@
return;
}
if (DBG) {
- Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", service " + ns);
+ if (discoveryRequest != null) {
+ Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", discovery "
+ + discoveryRequest);
+ } else {
+ Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", service " + ns);
+ }
}
switch (what) {
case DISCOVER_SERVICES_STARTED:
- final String s = getNsdServiceInfoType((NsdServiceInfo) obj);
+ final String s = getNsdServiceInfoType((DiscoveryRequest) obj);
executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStarted(s));
break;
case DISCOVER_SERVICES_FAILED:
removeListener(key);
executor.execute(() -> ((DiscoveryListener) listener).onStartDiscoveryFailed(
- getNsdServiceInfoType(ns), errorCode));
+ getNsdServiceInfoType(discoveryRequest), errorCode));
break;
case SERVICE_FOUND:
executor.execute(() -> ((DiscoveryListener) listener).onServiceFound(
@@ -1039,12 +1093,12 @@
// the effect for the client is indistinguishable from STOP_DISCOVERY_SUCCEEDED
removeListener(key);
executor.execute(() -> ((DiscoveryListener) listener).onStopDiscoveryFailed(
- getNsdServiceInfoType(ns), errorCode));
+ getNsdServiceInfoType(discoveryRequest), errorCode));
break;
case STOP_DISCOVERY_SUCCEEDED:
removeListener(key);
executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStopped(
- getNsdServiceInfoType(ns)));
+ getNsdServiceInfoType(discoveryRequest)));
break;
case REGISTER_SERVICE_FAILED:
removeListener(key);
@@ -1117,21 +1171,33 @@
return mListenerKey;
}
- // Assert that the listener is not in the map, then add it and returns its key
- private int putListener(Object listener, Executor e, NsdServiceInfo s) {
- checkListener(listener);
- final int key;
+ private int putListener(Object listener, Executor e, NsdServiceInfo serviceInfo) {
synchronized (mMapLock) {
- int valueIndex = mListenerMap.indexOfValue(listener);
+ return putListener(listener, e, mServiceMap, serviceInfo);
+ }
+ }
+
+ private int putListener(Object listener, Executor e, DiscoveryRequest discoveryRequest) {
+ synchronized (mMapLock) {
+ return putListener(listener, e, mDiscoveryMap, discoveryRequest);
+ }
+ }
+
+ // Assert that the listener is not in the map, then add it and returns its key
+ private <T> int putListener(Object listener, Executor e, SparseArray<T> map, T value) {
+ synchronized (mMapLock) {
+ checkListener(listener);
+ final int key;
+ final int valueIndex = mListenerMap.indexOfValue(listener);
if (valueIndex != -1) {
throw new IllegalArgumentException("listener already in use");
}
key = nextListenerKey();
mListenerMap.put(key, listener);
- mServiceMap.put(key, s);
+ map.put(key, value);
mExecutorMap.put(key, e);
+ return key;
}
- return key;
}
private int updateRegisteredListener(Object listener, Executor e, NsdServiceInfo s) {
@@ -1148,6 +1214,7 @@
synchronized (mMapLock) {
mListenerMap.remove(key);
mServiceMap.remove(key);
+ mDiscoveryMap.remove(key);
mExecutorMap.remove(key);
}
}
@@ -1163,9 +1230,9 @@
}
}
- private static String getNsdServiceInfoType(NsdServiceInfo s) {
- if (s == null) return "?";
- return s.getServiceType();
+ private static String getNsdServiceInfoType(DiscoveryRequest r) {
+ if (r == null) return "?";
+ return r.getServiceType();
}
/**
@@ -1208,7 +1275,7 @@
*/
public void registerService(@NonNull NsdServiceInfo serviceInfo, int protocolType,
@NonNull Executor executor, @NonNull RegistrationListener listener) {
- checkServiceInfo(serviceInfo);
+ checkServiceInfoForRegistration(serviceInfo);
checkProtocol(protocolType);
final AdvertisingRequest.Builder builder = new AdvertisingRequest.Builder(serviceInfo,
protocolType);
@@ -1267,7 +1334,10 @@
* @return Type and comma-separated list of subtypes, or null if invalid format.
*/
@Nullable
- private static Pair<String, String> getTypeAndSubtypes(@NonNull String typeWithSubtype) {
+ private static Pair<String, String> getTypeAndSubtypes(@Nullable String typeWithSubtype) {
+ if (typeWithSubtype == null) {
+ return null;
+ }
final Matcher matcher = Pattern.compile(TYPE_REGEX).matcher(typeWithSubtype);
if (!matcher.matches()) return null;
// Reject specifications using leading subtypes with a dot
@@ -1298,10 +1368,7 @@
@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);
+ checkServiceInfoForRegistration(serviceInfo);
checkProtocol(protocolType);
final int key;
// For update only request, the old listener has to be reused
@@ -1406,15 +1473,44 @@
if (TextUtils.isEmpty(serviceType)) {
throw new IllegalArgumentException("Service type cannot be empty");
}
- checkProtocol(protocolType);
+ DiscoveryRequest request = new DiscoveryRequest.Builder(protocolType, serviceType)
+ .setNetwork(network).build();
+ discoverServices(request, executor, listener);
+ }
- NsdServiceInfo s = new NsdServiceInfo();
- s.setServiceType(serviceType);
- s.setNetwork(network);
-
- int key = putListener(listener, executor, s);
+ /**
+ * Initiates service discovery to browse for instances of a service type. Service discovery
+ * consumes network bandwidth and will continue until the application calls
+ * {@link #stopServiceDiscovery}.
+ *
+ * <p> The function call immediately returns after sending a request to start service
+ * discovery to the framework. The application is notified of a success to initiate
+ * discovery through the callback {@link DiscoveryListener#onDiscoveryStarted} or a failure
+ * through {@link DiscoveryListener#onStartDiscoveryFailed}.
+ *
+ * <p> Upon successful start, application is notified when a service is found with
+ * {@link DiscoveryListener#onServiceFound} or when a service is lost with
+ * {@link DiscoveryListener#onServiceLost}.
+ *
+ * <p> Upon failure to start, service discovery is not active and application does
+ * not need to invoke {@link #stopServiceDiscovery}
+ *
+ * <p> The application should call {@link #stopServiceDiscovery} when discovery of this
+ * service type is no longer required, and/or whenever the application is paused or
+ * stopped.
+ *
+ * @param discoveryRequest the {@link DiscoveryRequest} object which specifies the discovery
+ * parameters such as service type, subtype and network
+ * @param executor Executor to run listener callbacks with
+ * @param listener The listener notifies of a successful discovery and is used
+ * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}.
+ */
+ @FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+ public void discoverServices(@NonNull DiscoveryRequest discoveryRequest,
+ @NonNull Executor executor, @NonNull DiscoveryListener listener) {
+ int key = putListener(listener, executor, discoveryRequest);
try {
- mService.discoverServices(key, s);
+ mService.discoverServices(key, discoveryRequest);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
@@ -1465,12 +1561,10 @@
throw new IllegalArgumentException("Service type cannot be empty");
}
Objects.requireNonNull(networkRequest, "NetworkRequest cannot be null");
- checkProtocol(protocolType);
+ DiscoveryRequest discoveryRequest =
+ new DiscoveryRequest.Builder(protocolType, serviceType).build();
- NsdServiceInfo s = new NsdServiceInfo();
- s.setServiceType(serviceType);
-
- final int baseListenerKey = putListener(listener, executor, s);
+ final int baseListenerKey = putListener(listener, executor, discoveryRequest);
final PerNetworkDiscoveryTracker discoveryInfo = new PerNetworkDiscoveryTracker(
serviceType, protocolType, executor, listener);
@@ -1551,7 +1645,7 @@
@Deprecated
public void resolveService(@NonNull NsdServiceInfo serviceInfo,
@NonNull Executor executor, @NonNull ResolveListener listener) {
- checkServiceInfo(serviceInfo);
+ checkServiceInfoForResolution(serviceInfo);
int key = putListener(listener, executor, serviceInfo);
try {
mService.resolveService(key, serviceInfo);
@@ -1602,9 +1696,10 @@
* @param executor Executor to run callbacks with
* @param listener to receive callback upon service update
*/
+ // TODO: use {@link DiscoveryRequest} to specify the service to be subscribed
public void registerServiceInfoCallback(@NonNull NsdServiceInfo serviceInfo,
@NonNull Executor executor, @NonNull ServiceInfoCallback listener) {
- checkServiceInfo(serviceInfo);
+ checkServiceInfoForResolution(serviceInfo);
int key = putListener(listener, executor, serviceInfo);
try {
mService.registerServiceInfoCallback(key, serviceInfo);
@@ -1643,13 +1738,13 @@
Objects.requireNonNull(listener, "listener cannot be null");
}
- private static void checkProtocol(int protocolType) {
+ static void checkProtocol(int protocolType) {
if (protocolType != PROTOCOL_DNS_SD) {
throw new IllegalArgumentException("Unsupported protocol");
}
}
- private static void checkServiceInfo(NsdServiceInfo serviceInfo) {
+ private static void checkServiceInfoForResolution(NsdServiceInfo serviceInfo) {
Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null");
if (TextUtils.isEmpty(serviceInfo.getServiceName())) {
throw new IllegalArgumentException("Service name cannot be empty");
@@ -1658,4 +1753,46 @@
throw new IllegalArgumentException("Service type cannot be empty");
}
}
+
+ /**
+ * Check if the {@link NsdServiceInfo} is valid for registration.
+ *
+ * The following can be registered:
+ * - A service with an optional host.
+ * - A hostname with addresses.
+ *
+ * Note that:
+ * - When registering a service, the service name, service type and port must be specified. If
+ * hostname is specified, the host addresses can optionally be specified.
+ * - When registering a host without a service, the addresses must be specified.
+ *
+ * @hide
+ */
+ public static void checkServiceInfoForRegistration(NsdServiceInfo serviceInfo) {
+ Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null");
+ boolean hasServiceName = !TextUtils.isEmpty(serviceInfo.getServiceName());
+ boolean hasServiceType = !TextUtils.isEmpty(serviceInfo.getServiceType());
+ boolean hasHostname = !TextUtils.isEmpty(serviceInfo.getHostname());
+ boolean hasHostAddresses = !CollectionUtils.isEmpty(serviceInfo.getHostAddresses());
+
+ if (serviceInfo.getPort() < 0) {
+ throw new IllegalArgumentException("Invalid port");
+ }
+
+ if (hasServiceType || hasServiceName || (serviceInfo.getPort() > 0)) {
+ if (!(hasServiceType && hasServiceName && (serviceInfo.getPort() > 0))) {
+ throw new IllegalArgumentException(
+ "The service type, service name or port is missing");
+ }
+ }
+
+ if (!hasServiceType && !hasHostname) {
+ throw new IllegalArgumentException("No service or host specified in NsdServiceInfo");
+ }
+
+ if (!hasServiceType && hasHostname && !hasHostAddresses) {
+ // TODO: b/317946010 - This may be allowed when it supports registering KEY RR.
+ throw new IllegalArgumentException("No host addresses specified in NsdServiceInfo");
+ }
+ }
}
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index ac4ea23..9491a9c 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -16,7 +16,7 @@
package android.net.nsd;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static com.android.net.module.util.HexDump.toHexString;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
@@ -35,11 +35,13 @@
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.StringJoiner;
/**
* A class representing service information for network service discovery
@@ -49,8 +51,10 @@
private static final String TAG = "NsdServiceInfo";
+ @Nullable
private String mServiceName;
+ @Nullable
private String mServiceType;
private final Set<String> mSubtypes;
@@ -59,6 +63,9 @@
private final List<InetAddress> mHostAddresses;
+ @Nullable
+ private String mHostname;
+
private int mPort;
@Nullable
@@ -66,6 +73,11 @@
private int mInterfaceIndex;
+ // The timestamp that one or more resource records associated with this service are considered
+ // invalid.
+ @Nullable
+ private Instant mExpirationTime;
+
public NsdServiceInfo() {
mSubtypes = new ArraySet<>();
mTxtRecord = new ArrayMap<>();
@@ -90,9 +102,11 @@
mSubtypes = new ArraySet<>(other.getSubtypes());
mTxtRecord = new ArrayMap<>(other.mTxtRecord);
mHostAddresses = new ArrayList<>(other.getHostAddresses());
+ mHostname = other.getHostname();
mPort = other.getPort();
mNetwork = other.getNetwork();
mInterfaceIndex = other.getInterfaceIndex();
+ mExpirationTime = other.getExpirationTime();
}
/** Get the service name */
@@ -169,6 +183,43 @@
}
/**
+ * Get the hostname.
+ *
+ * <p>When a service is resolved, it returns the hostname of the resolved service . The top
+ * level domain ".local." is omitted.
+ *
+ * <p>For example, it returns "MyHost" when the service's hostname is "MyHost.local.".
+ *
+ * @hide
+ */
+// @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_HOSTNAME_ENABLED)
+ @Nullable
+ public String getHostname() {
+ return mHostname;
+ }
+
+ /**
+ * Set a custom hostname for this service instance for registration.
+ *
+ * <p>A hostname must be in ".local." domain. The ".local." must be omitted when calling this
+ * method.
+ *
+ * <p>For example, you should call setHostname("MyHost") to use the hostname "MyHost.local.".
+ *
+ * <p>If a hostname is set with this method, the addresses set with {@link #setHostAddresses}
+ * will be registered with the hostname.
+ *
+ * <p>If the hostname is null (which is the default for a new {@link NsdServiceInfo}), a random
+ * hostname is used and the addresses of this device will be registered.
+ *
+ * @hide
+ */
+// @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_HOSTNAME_ENABLED)
+ public void setHostname(@Nullable String hostname) {
+ mHostname = hostname;
+ }
+
+ /**
* Unpack txt information from a base-64 encoded byte array.
*
* @param txtRecordsRawBytes The raw base64 encoded byte array.
@@ -447,6 +498,40 @@
return Collections.unmodifiableSet(mSubtypes);
}
+ /**
+ * Sets the timestamp after when this service is expired.
+ *
+ * Note: the value after the decimal point (in unit of seconds) will be discarded. For
+ * example, {@code 30} seconds will be used when {@code Duration.ofSeconds(30L, 50_000L)}
+ * is provided.
+ *
+ * @hide
+ */
+ public void setExpirationTime(@Nullable Instant expirationTime) {
+ if (expirationTime == null) {
+ mExpirationTime = null;
+ } else {
+ mExpirationTime = Instant.ofEpochSecond(expirationTime.getEpochSecond());
+ }
+ }
+
+ /**
+ * Returns the timestamp after when this service is expired or {@code null} if it's unknown.
+ *
+ * A service is considered expired if any of its DNS record is expired.
+ *
+ * Clients that are depending on the refreshness of the service information should not continue
+ * use this service after the returned timestamp. Instead, clients may re-send queries for the
+ * service to get updated the service information.
+ *
+ * @hide
+ */
+ // @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_TTL_ENABLED)
+ @Nullable
+ public Instant getExpirationTime() {
+ return mExpirationTime;
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
@@ -454,14 +539,55 @@
.append(", type: ").append(mServiceType)
.append(", subtypes: ").append(TextUtils.join(", ", mSubtypes))
.append(", hostAddresses: ").append(TextUtils.join(", ", mHostAddresses))
+ .append(", hostname: ").append(mHostname)
.append(", port: ").append(mPort)
- .append(", network: ").append(mNetwork);
+ .append(", network: ").append(mNetwork)
+ .append(", expirationTime: ").append(mExpirationTime);
- byte[] txtRecord = getTxtRecord();
- sb.append(", txtRecord: ").append(new String(txtRecord, StandardCharsets.UTF_8));
+ final StringJoiner txtJoiner =
+ new StringJoiner(", " /* delimiter */, "{" /* prefix */, "}" /* suffix */);
+
+ sb.append(", txtRecord: ");
+ for (int i = 0; i < mTxtRecord.size(); i++) {
+ txtJoiner.add(mTxtRecord.keyAt(i) + "=" + getPrintableTxtValue(mTxtRecord.valueAt(i)));
+ }
+ sb.append(txtJoiner.toString());
return sb.toString();
}
+ /**
+ * Returns printable string for {@code txtValue}.
+ *
+ * If {@code txtValue} contains non-printable ASCII characters, a HEX string with prefix "0x"
+ * will be returned. Otherwise, the ASCII string of {@code txtValue} is returned.
+ *
+ */
+ private static String getPrintableTxtValue(@Nullable byte[] txtValue) {
+ if (txtValue == null) {
+ return "(null)";
+ }
+
+ if (containsNonPrintableChars(txtValue)) {
+ return "0x" + toHexString(txtValue);
+ }
+
+ return new String(txtValue, StandardCharsets.US_ASCII);
+ }
+
+ /**
+ * Returns {@code true} if {@code txtValue} contains non-printable ASCII characters.
+ *
+ * The printable characters are in range of [32, 126].
+ */
+ private static boolean containsNonPrintableChars(byte[] txtValue) {
+ for (int i = 0; i < txtValue.length; i++) {
+ if (txtValue[i] < 32 || txtValue[i] > 126) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/** Implement the Parcelable interface */
public int describeContents() {
return 0;
@@ -494,6 +620,8 @@
for (InetAddress address : mHostAddresses) {
InetAddressUtils.parcelInetAddress(dest, address, flags);
}
+ dest.writeString(mHostname);
+ dest.writeLong(mExpirationTime != null ? mExpirationTime.getEpochSecond() : -1);
}
/** Implement the Parcelable interface */
@@ -523,6 +651,9 @@
for (int i = 0; i < size; i++) {
info.mHostAddresses.add(InetAddressUtils.unparcelInetAddress(in));
}
+ info.mHostname = in.readString();
+ final long seconds = in.readLong();
+ info.setExpirationTime(seconds < 0 ? null : Instant.ofEpochSecond(seconds));
return info;
}
diff --git a/framework/Android.bp b/framework/Android.bp
index f3d8689..8787167 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -95,6 +96,7 @@
],
impl_only_static_libs: [
"net-utils-device-common-bpf",
+ "net-utils-device-common-struct-base",
],
libs: [
"androidx.annotation_annotation",
@@ -105,9 +107,6 @@
apex_available: [
"com.android.tethering",
],
- lint: {
- strict_updatability_linting: true,
- },
}
java_library {
@@ -126,6 +125,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-base",
],
libs: [
// This cannot be in the defaults clause above because if it were, it would be used
@@ -137,9 +137,6 @@
"framework-wifi.stubs.module_lib",
],
visibility: ["//packages/modules/Connectivity:__subpackages__"],
- lint: {
- baseline_filename: "lint-baseline.xml",
- },
}
java_defaults {
@@ -170,17 +167,16 @@
"//packages/modules/Connectivity/framework-t",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/service-t",
- "//frameworks/base/packages/Connectivity/service",
"//frameworks/base",
// Tests using hidden APIs
"//cts/tests/netlegacy22.api",
"//cts/tests/tests/app.usage", // NetworkUsageStatsTest
"//external/sl4a:__subpackages__",
- "//frameworks/base/packages/Connectivity/tests:__subpackages__",
"//frameworks/base/core/tests/bandwidthtests",
"//frameworks/base/core/tests/benchmarks",
"//frameworks/base/core/tests/utillib",
+ "//frameworks/base/services/tests/VpnTests",
"//frameworks/base/tests/vcn",
"//frameworks/opt/net/ethernet/tests:__subpackages__",
"//frameworks/opt/telephony/tests/telephonytests",
@@ -198,6 +194,9 @@
lint: {
baseline_filename: "lint-baseline.xml",
},
+ aconfig_declarations: [
+ "com.android.net.flags-aconfig",
+ ],
}
platform_compat_config {
@@ -257,9 +256,6 @@
apex_available: [
"com.android.tethering",
],
- lint: {
- baseline_filename: "lint-baseline.xml",
- },
}
java_genrule {
@@ -320,9 +316,6 @@
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
@@ -347,7 +340,4 @@
visibility: [
"//packages/modules/Connectivity/Tethering:__subpackages__",
],
- lint: {
- baseline_filename: "lint-baseline.xml",
- },
}
diff --git a/framework/aidl-export/android/net/TetheringManager.aidl b/framework/aidl-export/android/net/TetheringManager.aidl
new file mode 100644
index 0000000..1235722
--- /dev/null
+++ b/framework/aidl-export/android/net/TetheringManager.aidl
@@ -0,0 +1,20 @@
+/**
+ *
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable TetheringManager.TetheringRequest;
diff --git a/framework/aidl-export/android/net/nsd/DiscoveryRequest.aidl b/framework/aidl-export/android/net/nsd/DiscoveryRequest.aidl
new file mode 100644
index 0000000..481a066
--- /dev/null
+++ b/framework/aidl-export/android/net/nsd/DiscoveryRequest.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 DiscoveryRequest;
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 6860c3c..ef8415c 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -315,6 +315,7 @@
method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
method public int getOwnerUid();
method public int getSignalStrength();
+ method @FlaggedApi("com.android.net.flags.request_restricted_wifi") @NonNull public java.util.Set<java.lang.Integer> getSubscriptionIds();
method @Nullable public android.net.TransportInfo getTransportInfo();
method public boolean hasCapability(int);
method public boolean hasEnterpriseId(int);
@@ -332,6 +333,7 @@
field public static final int NET_CAPABILITY_IA = 7; // 0x7
field public static final int NET_CAPABILITY_IMS = 4; // 0x4
field public static final int NET_CAPABILITY_INTERNET = 12; // 0xc
+ field @FlaggedApi("com.android.net.flags.net_capability_local_network") public static final int NET_CAPABILITY_LOCAL_NETWORK = 36; // 0x24
field public static final int NET_CAPABILITY_MCX = 23; // 0x17
field public static final int NET_CAPABILITY_MMS = 0; // 0x0
field public static final int NET_CAPABILITY_MMTEL = 33; // 0x21
@@ -360,6 +362,7 @@
field public static final int TRANSPORT_CELLULAR = 0; // 0x0
field public static final int TRANSPORT_ETHERNET = 3; // 0x3
field public static final int TRANSPORT_LOWPAN = 6; // 0x6
+ field @FlaggedApi("com.android.net.flags.support_transport_satellite") public static final int TRANSPORT_SATELLITE = 10; // 0xa
field public static final int TRANSPORT_THREAD = 9; // 0x9
field public static final int TRANSPORT_USB = 8; // 0x8
field public static final int TRANSPORT_VPN = 4; // 0x4
@@ -418,6 +421,7 @@
method public int describeContents();
method @NonNull public int[] getCapabilities();
method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
+ method @FlaggedApi("com.android.net.flags.request_restricted_wifi") @NonNull public java.util.Set<java.lang.Integer> getSubscriptionIds();
method @NonNull public int[] getTransportTypes();
method public boolean hasCapability(int);
method public boolean hasTransport(int);
@@ -437,6 +441,7 @@
method @NonNull public android.net.NetworkRequest.Builder setIncludeOtherUidNetworks(boolean);
method @Deprecated public android.net.NetworkRequest.Builder setNetworkSpecifier(String);
method public android.net.NetworkRequest.Builder setNetworkSpecifier(android.net.NetworkSpecifier);
+ method @FlaggedApi("com.android.net.flags.request_restricted_wifi") @NonNull public android.net.NetworkRequest.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
}
public class ParseException extends java.lang.RuntimeException {
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index e812024..bef29a4 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -307,7 +307,6 @@
method @NonNull public int[] getAdministratorUids();
method @Nullable public static String getCapabilityCarrierName(int);
method @Nullable public String getSsid();
- method @NonNull public java.util.Set<java.lang.Integer> getSubscriptionIds();
method @NonNull public int[] getTransportTypes();
method @Nullable public java.util.List<android.net.Network> getUnderlyingNetworks();
method public boolean isPrivateDnsBroken();
@@ -373,7 +372,6 @@
public static class NetworkRequest.Builder {
method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP) public android.net.NetworkRequest.Builder setSignalStrength(int);
- method @NonNull public android.net.NetworkRequest.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
}
public final class NetworkScore implements android.os.Parcelable {
diff --git a/framework/jni/android_net_NetworkUtils.cpp b/framework/jni/android_net_NetworkUtils.cpp
index 5403be7..3779a00 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,14 @@
}
}
+static jboolean android_net_utils_isKernel64Bit(JNIEnv *env, jclass clazz) {
+ return bpf::isKernel64Bit();
+}
+
+static jboolean android_net_utils_isKernelX86(JNIEnv *env, jclass clazz) {
+ return bpf::isX86();
+}
+
// ----------------------------------------------------------------------------
/*
@@ -272,6 +281,8 @@
{ "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 },
+ { "isKernelX86", "()Z", (void*) android_net_utils_isKernelX86 },
};
// clang-format on
diff --git a/framework/lint-baseline.xml b/framework/lint-baseline.xml
index f68aad7..2c0b15f 100644
--- a/framework/lint-baseline.xml
+++ b/framework/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
<issue
id="NewApi"
@@ -8,78 +8,177 @@
errorLine2=" ~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
- line="2456"
+ line="2490"
column="71"/>
</issue>
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.Proxy#setHttpProxyConfiguration`"
- errorLine1=" Proxy.setHttpProxyConfiguration(getInstance().getDefaultProxy());"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
- line="5323"
- column="23"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
- errorLine1=" if (!Build.isDebuggable()) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/ConnectivitySettingsManager.java"
- line="1072"
- column="24"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
- errorLine1=" final int end = nextUser.getUid(0 /* appId */) - 1;"
- errorLine2=" ~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
- line="50"
- column="34"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
- errorLine1=" final int start = user.getUid(0 /* appId */);"
- errorLine2=" ~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
- line="49"
- column="32"/>
- </issue>
-
- <issue
- id="NewApi"
message="Call requires API level 31 (current min is 30): `android.provider.Settings#checkAndNoteWriteSettingsOperation`"
errorLine1=" return Settings.checkAndNoteWriteSettingsOperation(context, uid, callingPackage,"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
- line="2799"
+ line="2853"
column="25"/>
</issue>
<issue
id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.Proxy#setHttpProxyConfiguration`"
+ errorLine1=" Proxy.setHttpProxyConfiguration(getInstance().getDefaultProxy());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="5422"
+ column="23"/>
+ </issue>
+
+ <issue
+ id="NewApi"
message="Call requires API level 31 (current min is 30): `java.net.InetAddress#clearDnsCache`"
errorLine1=" InetAddress.clearDnsCache();"
errorLine2=" ~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
- line="5329"
+ line="5428"
column="25"/>
</issue>
<issue
id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#dispatchNetworkConfigurationChange`"
+ errorLine1=" NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="5431"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#getInstance`"
+ errorLine1=" NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="5431"
+ column="36"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+ errorLine1=" if (!Build.isDebuggable()) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivitySettingsManager.java"
+ line="1095"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 30): `android.system.OsConstants#ENONET`"
+ errorLine1=' new DnsException(ERROR_SYSTEM, new ErrnoException("resNetworkQuery", ENONET))));'
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/DnsResolver.java"
+ line="367"
+ column="90"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
+ line="181"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
+ line="373"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(is);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="171"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(zos);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="178"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(bis);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="401"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(bos);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="416"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#isNumericAddress`"
+ errorLine1=" return InetAddressUtils.isNumericAddress(address);"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
+ line="46"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#parseNumericAddress`"
+ errorLine1=" return InetAddressUtils.parseNumericAddress(address);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
+ line="63"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
message="Call requires API level 31 (current min is 30): `java.net.InetAddress#getAllByNameOnNet`"
errorLine1=" return InetAddress.getAllByNameOnNet(host, getNetIdForResolv());"
errorLine2=" ~~~~~~~~~~~~~~~~~">
@@ -103,17 +202,6 @@
<issue
id="NewApi"
message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(is);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
- line="168"
- column="33"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
errorLine1=" if (failed) IoUtils.closeQuietly(socket);"
errorLine2=" ~~~~~~~~~~~~">
<location
@@ -157,105 +245,6 @@
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(bis);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
- line="391"
- column="21"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(bos);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
- line="406"
- column="21"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(socket);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
- line="181"
- column="21"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(socket);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
- line="373"
- column="21"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(zos);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
- line="175"
- column="21"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#isNumericAddress`"
- errorLine1=" return InetAddressUtils.isNumericAddress(address);"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
- line="46"
- column="33"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#parseNumericAddress`"
- errorLine1=" return InetAddressUtils.parseNumericAddress(address);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
- line="63"
- column="33"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#dispatchNetworkConfigurationChange`"
- errorLine1=" NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
- line="5332"
- column="50"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#getInstance`"
- errorLine1=" NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
- line="5332"
- column="36"/>
- </issue>
-
- <issue
- id="NewApi"
message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#createInstance`"
errorLine1=" HttpURLConnectionFactory urlConnectionFactory = HttpURLConnectionFactory.createInstance();"
errorLine2=" ~~~~~~~~~~~~~~">
@@ -267,17 +256,6 @@
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#openConnection`"
- errorLine1=" return urlConnectionFactory.openConnection(url, socketFactory, proxy);"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/Network.java"
- line="372"
- column="37"/>
- </issue>
-
- <issue
- id="NewApi"
message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#setDns`"
errorLine1=" urlConnectionFactory.setDns(dnsLookup); // Let traffic go via dnsLookup"
errorLine2=" ~~~~~~">
@@ -300,35 +278,13 @@
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `new android.net.EthernetNetworkSpecifier`"
- errorLine1=" return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#openConnection`"
+ errorLine1=" return urlConnectionFactory.openConnection(url, socketFactory, proxy);"
+ errorLine2=" ~~~~~~~~~~~~~~">
<location
- file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
- line="525"
- column="48"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
- errorLine1=" return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
- line="525"
- column="48"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Class requires API level 31 (current min is 30): `android.telephony.data.EpsBearerQosSessionAttributes`"
- errorLine1=" (EpsBearerQosSessionAttributes)attributes));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
- line="1421"
- column="22"/>
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="372"
+ column="37"/>
</issue>
<issue
@@ -338,18 +294,18 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
- line="1418"
+ line="1462"
column="35"/>
</issue>
<issue
id="NewApi"
- message="Class requires API level 31 (current min is 30): `android.telephony.data.NrQosSessionAttributes`"
- errorLine1=" (NrQosSessionAttributes)attributes));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ message="Class requires API level 31 (current min is 30): `android.telephony.data.EpsBearerQosSessionAttributes`"
+ errorLine1=" (EpsBearerQosSessionAttributes)attributes));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
- line="1425"
+ line="1465"
column="22"/>
</issue>
@@ -360,8 +316,63 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
- line="1422"
+ line="1466"
column="42"/>
</issue>
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.telephony.data.NrQosSessionAttributes`"
+ errorLine1=" (NrQosSessionAttributes)attributes));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
+ line="1469"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.EthernetNetworkSpecifier`"
+ errorLine1=" return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
+ line="553"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
+ errorLine1=" return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
+ line="553"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+ errorLine1=" final int start = user.getUid(0 /* appId */);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
+ line="49"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+ errorLine1=" final int end = nextUser.getUid(0 /* appId */) - 1;"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
+ line="50"
+ column="34"/>
+ </issue>
+
</issues>
\ No newline at end of file
diff --git a/framework/src/android/net/BpfNetMapsReader.java b/framework/src/android/net/BpfNetMapsReader.java
deleted file mode 100644
index ee422ab..0000000
--- a/framework/src/android/net/BpfNetMapsReader.java
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net;
-
-import static android.net.BpfNetMapsConstants.CONFIGURATION_MAP_PATH;
-import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED;
-import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY;
-import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
-import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
-import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
-import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
-import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
-import static android.net.BpfNetMapsUtils.getMatchByFirewallChain;
-import static android.net.BpfNetMapsUtils.isFirewallAllowList;
-import static android.net.BpfNetMapsUtils.throwIfPreT;
-import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
-import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
-
-import android.annotation.NonNull;
-import android.annotation.RequiresApi;
-import android.os.Build;
-import android.os.ServiceSpecificException;
-import android.system.ErrnoException;
-import android.system.Os;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.modules.utils.build.SdkLevel;
-import com.android.net.module.util.BpfMap;
-import com.android.net.module.util.IBpfMap;
-import com.android.net.module.util.Struct.S32;
-import com.android.net.module.util.Struct.U32;
-import com.android.net.module.util.Struct.U8;
-
-/**
- * A helper class to *read* java BpfMaps.
- * @hide
- */
-@RequiresApi(Build.VERSION_CODES.TIRAMISU) // BPF maps were only mainlined in T
-public class BpfNetMapsReader {
- private static final String TAG = BpfNetMapsReader.class.getSimpleName();
-
- // Locally store the handle of bpf maps. The FileDescriptors are statically cached inside the
- // BpfMap implementation.
-
- // Bpf map to store various networking configurations, the format of the value is different
- // for different keys. See BpfNetMapsConstants#*_CONFIGURATION_KEY for keys.
- private final IBpfMap<S32, U32> mConfigurationMap;
- // Bpf map to store per uid traffic control configurations.
- // See {@link UidOwnerValue} for more detail.
- private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap;
- private final IBpfMap<S32, U8> mDataSaverEnabledMap;
- private final Dependencies mDeps;
-
- // Bitmaps for calculating whether a given uid is blocked by firewall chains.
- private static final long sMaskDropIfSet;
- private static final long sMaskDropIfUnset;
-
- static {
- long maskDropIfSet = 0L;
- long maskDropIfUnset = 0L;
-
- for (int chain : BpfNetMapsConstants.ALLOW_CHAINS) {
- final long match = getMatchByFirewallChain(chain);
- maskDropIfUnset |= match;
- }
- for (int chain : BpfNetMapsConstants.DENY_CHAINS) {
- final long match = getMatchByFirewallChain(chain);
- maskDropIfSet |= match;
- }
- sMaskDropIfSet = maskDropIfSet;
- sMaskDropIfUnset = maskDropIfUnset;
- }
-
- private static class SingletonHolder {
- static final BpfNetMapsReader sInstance = new BpfNetMapsReader();
- }
-
- @NonNull
- public static BpfNetMapsReader getInstance() {
- return SingletonHolder.sInstance;
- }
-
- private BpfNetMapsReader() {
- this(new Dependencies());
- }
-
- // While the production code uses the singleton to optimize for performance and deal with
- // concurrent access, the test needs to use a non-static approach for dependency injection and
- // mocking virtual bpf maps.
- @VisibleForTesting
- public BpfNetMapsReader(@NonNull Dependencies deps) {
- if (!SdkLevel.isAtLeastT()) {
- throw new UnsupportedOperationException(
- BpfNetMapsReader.class.getSimpleName() + " is not supported below Android T");
- }
- mDeps = deps;
- mConfigurationMap = mDeps.getConfigurationMap();
- mUidOwnerMap = mDeps.getUidOwnerMap();
- mDataSaverEnabledMap = mDeps.getDataSaverEnabledMap();
- }
-
- /**
- * Dependencies of BpfNetMapReader, for injection in tests.
- */
- @VisibleForTesting
- public static class Dependencies {
- /** Get the configuration map. */
- public IBpfMap<S32, U32> getConfigurationMap() {
- try {
- return new BpfMap<>(CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDONLY,
- S32.class, U32.class);
- } catch (ErrnoException e) {
- throw new IllegalStateException("Cannot open configuration map", e);
- }
- }
-
- /** Get the uid owner map. */
- public IBpfMap<S32, UidOwnerValue> getUidOwnerMap() {
- try {
- return new BpfMap<>(UID_OWNER_MAP_PATH, BpfMap.BPF_F_RDONLY,
- S32.class, UidOwnerValue.class);
- } catch (ErrnoException e) {
- throw new IllegalStateException("Cannot open uid owner map", e);
- }
- }
-
- /** Get the data saver enabled map. */
- public IBpfMap<S32, U8> getDataSaverEnabledMap() {
- try {
- return new BpfMap<>(DATA_SAVER_ENABLED_MAP_PATH, BpfMap.BPF_F_RDONLY, S32.class,
- U8.class);
- } catch (ErrnoException e) {
- throw new IllegalStateException("Cannot open data saver enabled map", e);
- }
- }
- }
-
- /**
- * Get the specified firewall chain's status.
- *
- * @param chain target chain
- * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
- * @throws UnsupportedOperationException if called on pre-T devices.
- * @throws ServiceSpecificException in case of failure, with an error code indicating the
- * cause of the failure.
- */
- public boolean isChainEnabled(final int chain) {
- return isChainEnabled(mConfigurationMap, chain);
- }
-
- /**
- * Get firewall rule of specified firewall chain on specified uid.
- *
- * @param chain target chain
- * @param uid target uid
- * @return either {@link ConnectivityManager#FIREWALL_RULE_ALLOW} or
- * {@link ConnectivityManager#FIREWALL_RULE_DENY}.
- * @throws UnsupportedOperationException if called on pre-T devices.
- * @throws ServiceSpecificException in case of failure, with an error code indicating the
- * cause of the failure.
- */
- public int getUidRule(final int chain, final int uid) {
- return getUidRule(mUidOwnerMap, chain, uid);
- }
-
- /**
- * Get the specified firewall chain's status.
- *
- * @param configurationMap target configurationMap
- * @param chain target chain
- * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
- * @throws UnsupportedOperationException if called on pre-T devices.
- * @throws ServiceSpecificException in case of failure, with an error code indicating the
- * cause of the failure.
- */
- public static boolean isChainEnabled(
- final IBpfMap<S32, U32> configurationMap, final int chain) {
- throwIfPreT("isChainEnabled is not available on pre-T devices");
-
- final long match = getMatchByFirewallChain(chain);
- try {
- final U32 config = configurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
- return (config.val & match) != 0;
- } catch (ErrnoException e) {
- throw new ServiceSpecificException(e.errno,
- "Unable to get firewall chain status: " + Os.strerror(e.errno));
- }
- }
-
- /**
- * Get firewall rule of specified firewall chain on specified uid.
- *
- * @param uidOwnerMap target uidOwnerMap.
- * @param chain target chain.
- * @param uid target uid.
- * @return either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
- * @throws UnsupportedOperationException if called on pre-T devices.
- * @throws ServiceSpecificException in case of failure, with an error code indicating the
- * cause of the failure.
- */
- public static int getUidRule(final IBpfMap<S32, UidOwnerValue> uidOwnerMap,
- final int chain, final int uid) {
- throwIfPreT("getUidRule is not available on pre-T devices");
-
- final long match = getMatchByFirewallChain(chain);
- final boolean isAllowList = isFirewallAllowList(chain);
- try {
- final UidOwnerValue uidMatch = uidOwnerMap.getValue(new S32(uid));
- final boolean isMatchEnabled = uidMatch != null && (uidMatch.rule & match) != 0;
- return isMatchEnabled == isAllowList ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
- } catch (ErrnoException e) {
- throw new ServiceSpecificException(e.errno,
- "Unable to get uid rule status: " + Os.strerror(e.errno));
- }
- }
-
- /**
- * Return whether the network is blocked by firewall chains for the given uid.
- *
- * @param uid The target uid.
- * @param isNetworkMetered Whether the target network is metered.
- * @param isDataSaverEnabled Whether the data saver is enabled.
- *
- * @return True if the network is blocked. Otherwise, false.
- * @throws ServiceSpecificException if the read fails.
- *
- * @hide
- */
- public boolean isUidNetworkingBlocked(final int uid, boolean isNetworkMetered,
- boolean isDataSaverEnabled) {
- throwIfPreT("isUidBlockedByFirewallChains is not available on pre-T devices");
-
- final long uidRuleConfig;
- final long uidMatch;
- try {
- uidRuleConfig = mConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val;
- final UidOwnerValue value = mUidOwnerMap.getValue(new S32(uid));
- uidMatch = (value != null) ? value.rule : 0L;
- } catch (ErrnoException e) {
- throw new ServiceSpecificException(e.errno,
- "Unable to get firewall chain status: " + Os.strerror(e.errno));
- }
-
- final boolean blockedByAllowChains = 0 != (uidRuleConfig & ~uidMatch & sMaskDropIfUnset);
- final boolean blockedByDenyChains = 0 != (uidRuleConfig & uidMatch & sMaskDropIfSet);
- if (blockedByAllowChains || blockedByDenyChains) {
- return true;
- }
-
- if (!isNetworkMetered) return false;
- if ((uidMatch & PENALTY_BOX_MATCH) != 0) return true;
- if ((uidMatch & HAPPY_BOX_MATCH) != 0) return false;
- return isDataSaverEnabled;
- }
-
- /**
- * Get Data Saver enabled or disabled
- *
- * @return whether Data Saver is enabled or disabled.
- * @throws ServiceSpecificException in case of failure, with an error code indicating the
- * cause of the failure.
- */
- public boolean getDataSaverEnabled() {
- throwIfPreT("getDataSaverEnabled is not available on pre-T devices");
-
- try {
- return mDataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val == DATA_SAVER_ENABLED;
- } catch (ErrnoException e) {
- throw new ServiceSpecificException(e.errno, "Unable to get data saver: "
- + Os.strerror(e.errno));
- }
- }
-}
diff --git a/framework/src/android/net/BpfNetMapsUtils.java b/framework/src/android/net/BpfNetMapsUtils.java
index 11d610c..19ecafb 100644
--- a/framework/src/android/net/BpfNetMapsUtils.java
+++ b/framework/src/android/net/BpfNetMapsUtils.java
@@ -18,17 +18,22 @@
import static android.net.BpfNetMapsConstants.ALLOW_CHAINS;
import static android.net.BpfNetMapsConstants.BACKGROUND_MATCH;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY;
import static android.net.BpfNetMapsConstants.DENY_CHAINS;
import static android.net.BpfNetMapsConstants.DOZABLE_MATCH;
+import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
import static android.net.BpfNetMapsConstants.LOW_POWER_STANDBY_MATCH;
import static android.net.BpfNetMapsConstants.MATCH_LIST;
import static android.net.BpfNetMapsConstants.NO_MATCH;
import static android.net.BpfNetMapsConstants.OEM_DENY_1_MATCH;
import static android.net.BpfNetMapsConstants.OEM_DENY_2_MATCH;
import static android.net.BpfNetMapsConstants.OEM_DENY_3_MATCH;
+import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
import static android.net.BpfNetMapsConstants.POWERSAVE_MATCH;
import static android.net.BpfNetMapsConstants.RESTRICTED_MATCH;
import static android.net.BpfNetMapsConstants.STANDBY_MATCH;
+import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
@@ -38,12 +43,22 @@
import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
import static android.system.OsConstants.EINVAL;
+import android.os.Process;
import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
+import android.system.Os;
import android.util.Pair;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.S32;
+import com.android.net.module.util.Struct.U32;
+import com.android.net.module.util.Struct.U8;
import java.util.StringJoiner;
@@ -56,6 +71,26 @@
// Because modules could have different copies of this class if this is statically linked,
// which would be problematic if the definitions in these modules are not synchronized.
public class BpfNetMapsUtils {
+ // Bitmaps for calculating whether a given uid is blocked by firewall chains.
+ private static final long sMaskDropIfSet;
+ private static final long sMaskDropIfUnset;
+
+ static {
+ long maskDropIfSet = 0L;
+ long maskDropIfUnset = 0L;
+
+ for (int chain : BpfNetMapsConstants.ALLOW_CHAINS) {
+ final long match = getMatchByFirewallChain(chain);
+ maskDropIfUnset |= match;
+ }
+ for (int chain : BpfNetMapsConstants.DENY_CHAINS) {
+ final long match = getMatchByFirewallChain(chain);
+ maskDropIfSet |= match;
+ }
+ sMaskDropIfSet = maskDropIfSet;
+ sMaskDropIfUnset = maskDropIfUnset;
+ }
+
// Prevent this class from being accidental instantiated.
private BpfNetMapsUtils() {}
@@ -125,14 +160,136 @@
return sj.toString();
}
- public static final boolean PRE_T = !SdkLevel.isAtLeastT();
-
/**
* Throw UnsupportedOperationException if SdkLevel is before T.
*/
public static void throwIfPreT(final String msg) {
- if (PRE_T) {
+ if (!SdkLevel.isAtLeastT()) {
throw new UnsupportedOperationException(msg);
}
}
+
+ /**
+ * Get the specified firewall chain's status.
+ *
+ * @param configurationMap target configurationMap
+ * @param chain target chain
+ * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public static boolean isChainEnabled(
+ final IBpfMap<S32, U32> configurationMap, final int chain) {
+ throwIfPreT("isChainEnabled is not available on pre-T devices");
+
+ final long match = getMatchByFirewallChain(chain);
+ try {
+ final U32 config = configurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
+ return (config.val & match) != 0;
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ "Unable to get firewall chain status: " + Os.strerror(e.errno));
+ }
+ }
+
+ /**
+ * Get firewall rule of specified firewall chain on specified uid.
+ *
+ * @param uidOwnerMap target uidOwnerMap.
+ * @param chain target chain.
+ * @param uid target uid.
+ * @return either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public static int getUidRule(final IBpfMap<S32, UidOwnerValue> uidOwnerMap,
+ final int chain, final int uid) {
+ throwIfPreT("getUidRule is not available on pre-T devices");
+
+ final long match = getMatchByFirewallChain(chain);
+ final boolean isAllowList = isFirewallAllowList(chain);
+ try {
+ final UidOwnerValue uidMatch = uidOwnerMap.getValue(new S32(uid));
+ final boolean isMatchEnabled = uidMatch != null && (uidMatch.rule & match) != 0;
+ return isMatchEnabled == isAllowList ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ "Unable to get uid rule status: " + Os.strerror(e.errno));
+ }
+ }
+
+ /**
+ * Return whether the network is blocked by firewall chains for the given uid.
+ *
+ * Note that {@link #getDataSaverEnabled(IBpfMap)} has a latency before V.
+ *
+ * @param uid The target uid.
+ * @param isNetworkMetered Whether the target network is metered.
+ *
+ * @return True if the network is blocked. Otherwise, false.
+ * @throws ServiceSpecificException if the read fails.
+ *
+ * @hide
+ */
+ public static boolean isUidNetworkingBlocked(final int uid, boolean isNetworkMetered,
+ IBpfMap<S32, U32> configurationMap,
+ IBpfMap<S32, UidOwnerValue> uidOwnerMap,
+ IBpfMap<S32, U8> dataSaverEnabledMap
+ ) {
+ throwIfPreT("isUidBlockedByFirewallChains is not available on pre-T devices");
+
+ // System uid is not blocked by firewall chains, see bpf_progs/netd.c
+ // TODO: use UserHandle.isCore() once it is accessible
+ if (uid < Process.FIRST_APPLICATION_UID) {
+ return false;
+ }
+
+ final long uidRuleConfig;
+ final long uidMatch;
+ try {
+ uidRuleConfig = configurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val;
+ final UidOwnerValue value = uidOwnerMap.getValue(new Struct.S32(uid));
+ uidMatch = (value != null) ? value.rule : 0L;
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ "Unable to get firewall chain status: " + Os.strerror(e.errno));
+ }
+
+ final boolean blockedByAllowChains = 0 != (uidRuleConfig & ~uidMatch & sMaskDropIfUnset);
+ final boolean blockedByDenyChains = 0 != (uidRuleConfig & uidMatch & sMaskDropIfSet);
+ if (blockedByAllowChains || blockedByDenyChains) {
+ return true;
+ }
+
+ if (!isNetworkMetered) return false;
+ if ((uidMatch & PENALTY_BOX_MATCH) != 0) return true;
+ if ((uidMatch & HAPPY_BOX_MATCH) != 0) return false;
+ return getDataSaverEnabled(dataSaverEnabledMap);
+ }
+
+ /**
+ * Get Data Saver enabled or disabled
+ *
+ * Note that before V, the data saver status in bpf is written by ConnectivityService
+ * when receiving {@link ConnectivityManager#ACTION_RESTRICT_BACKGROUND_CHANGED}. Thus,
+ * the status is not synchronized.
+ * On V+, the data saver status is set by platform code when enabling/disabling
+ * data saver, which is synchronized.
+ *
+ * @return whether Data Saver is enabled or disabled.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public static boolean getDataSaverEnabled(IBpfMap<S32, U8> dataSaverEnabledMap) {
+ throwIfPreT("getDataSaverEnabled is not available on pre-T devices");
+
+ try {
+ return dataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val == DATA_SAVER_ENABLED;
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno, "Unable to get data saver: "
+ + Os.strerror(e.errno));
+ }
+ }
}
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index fa27d0e..b1e636d 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -74,6 +74,7 @@
import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.build.SdkLevel;
import libcore.net.event.NetworkEventDispatcher;
@@ -6022,6 +6023,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
@@ -6271,16 +6279,21 @@
// Only the system server process and the network stack have access.
@FlaggedApi(Flags.SUPPORT_IS_UID_NETWORKING_BLOCKED)
@SystemApi(client = MODULE_LIBRARIES)
- @RequiresApi(Build.VERSION_CODES.TIRAMISU) // BPF maps were only mainlined in T
+ // Note b/326143935 kernel bug can trigger crash on some T device.
+ @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
- final BpfNetMapsReader reader = BpfNetMapsReader.getInstance();
+ if (!SdkLevel.isAtLeastU()) {
+ throw new IllegalStateException(
+ "isUidNetworkingBlocked is not supported on pre-U devices");
+ }
+ final NetworkStackBpfNetMaps reader = NetworkStackBpfNetMaps.getInstance();
// Note that before V, the data saver status in bpf is written by ConnectivityService
// when receiving {@link #ACTION_RESTRICT_BACKGROUND_CHANGED}. Thus,
// the status is not synchronized.
// On V+, the data saver status is set by platform code when enabling/disabling
// data saver, which is synchronized.
- return reader.isUidNetworkingBlocked(uid, isNetworkMetered, reader.getDataSaverEnabled());
+ return reader.isUidNetworkingBlocked(uid, isNetworkMetered);
}
/** @hide */
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index efae754..45efbfe 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -20,6 +20,7 @@
import static com.android.net.module.util.BitUtils.appendStringRepresentationOfBitMaskToStringBuilder;
import static com.android.net.module.util.BitUtils.describeDifferences;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.LongDef;
import android.annotation.NonNull;
@@ -29,9 +30,6 @@
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.net.ConnectivityManager.NetworkCallback;
-// Can't be imported because aconfig tooling doesn't exist on udc-mainline-prod yet
-// See inner class Flags which mimics this for the time being
-// import android.net.flags.Flags;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
@@ -130,6 +128,12 @@
public static class Flags {
static final String FLAG_FORBIDDEN_CAPABILITY =
"com.android.net.flags.forbidden_capability";
+ static final String FLAG_NET_CAPABILITY_LOCAL_NETWORK =
+ "com.android.net.flags.net_capability_local_network";
+ static final String REQUEST_RESTRICTED_WIFI =
+ "com.android.net.flags.request_restricted_wifi";
+ static final String SUPPORT_TRANSPORT_SATELLITE =
+ "com.android.net.flags.support_transport_satellite";
}
/**
@@ -716,17 +720,24 @@
public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35;
/**
- * This is a local network, e.g. a tethering downstream or a P2P direct network.
+ * Indicates that this network is a local network.
*
- * <p>
- * Note that local networks are not sent to callbacks by default. To receive callbacks about
- * them, the {@link NetworkRequest} instance must be prepared to see them, either by
- * adding the capability with {@link NetworkRequest.Builder#addCapability}, by removing
- * this forbidden capability with {@link NetworkRequest.Builder#removeForbiddenCapability},
- * or by clearing all capabilites with {@link NetworkRequest.Builder#clearCapabilities()}.
- * </p>
- * @hide
+ * Local networks are networks where the device is not obtaining IP addresses from the
+ * network, but advertising IP addresses itself. Examples of local networks are:
+ * <ul>
+ * <li>USB tethering or Wi-Fi hotspot networks to which the device is sharing its Internet
+ * connectivity.
+ * <li>Thread networks where the current device is the Thread Border Router.
+ * <li>Wi-Fi P2P networks where the current device is the Group Owner.
+ * </ul>
+ *
+ * Networks used to obtain Internet access are never local networks.
+ *
+ * Apps that target an SDK before {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} will not see
+ * networks with this capability unless they explicitly set the NET_CAPABILITY_LOCAL_NETWORK
+ * in their NetworkRequests.
*/
+ @FlaggedApi(Flags.FLAG_NET_CAPABILITY_LOCAL_NETWORK)
public static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_LOCAL_NETWORK;
@@ -738,22 +749,22 @@
* Network capabilities that are expected to be mutable, i.e., can change while a particular
* network is connected.
*/
- private static final long MUTABLE_CAPABILITIES = BitUtils.packBitList(
+ private static final long MUTABLE_CAPABILITIES =
// TRUSTED can change when user explicitly connects to an untrusted network in Settings.
// http://b/18206275
- NET_CAPABILITY_TRUSTED,
- NET_CAPABILITY_VALIDATED,
- NET_CAPABILITY_CAPTIVE_PORTAL,
- NET_CAPABILITY_NOT_ROAMING,
- NET_CAPABILITY_FOREGROUND,
- NET_CAPABILITY_NOT_CONGESTED,
- NET_CAPABILITY_NOT_SUSPENDED,
- NET_CAPABILITY_PARTIAL_CONNECTIVITY,
- NET_CAPABILITY_TEMPORARILY_NOT_METERED,
- NET_CAPABILITY_NOT_VCN_MANAGED,
+ (1L << NET_CAPABILITY_TRUSTED) |
+ (1L << NET_CAPABILITY_VALIDATED) |
+ (1L << NET_CAPABILITY_CAPTIVE_PORTAL) |
+ (1L << NET_CAPABILITY_NOT_ROAMING) |
+ (1L << NET_CAPABILITY_FOREGROUND) |
+ (1L << NET_CAPABILITY_NOT_CONGESTED) |
+ (1L << NET_CAPABILITY_NOT_SUSPENDED) |
+ (1L << NET_CAPABILITY_PARTIAL_CONNECTIVITY) |
+ (1L << NET_CAPABILITY_TEMPORARILY_NOT_METERED) |
+ (1L << NET_CAPABILITY_NOT_VCN_MANAGED) |
// The value of NET_CAPABILITY_HEAD_UNIT is 32, which cannot use int to do bit shift,
// otherwise there will be an overflow. Use long to do bit shift instead.
- NET_CAPABILITY_HEAD_UNIT);
+ (1L << NET_CAPABILITY_HEAD_UNIT);
/**
* Network capabilities that are not allowed in NetworkRequests. This exists because the
@@ -773,10 +784,10 @@
/**
* Capabilities that are set by default when the object is constructed.
*/
- private static final long DEFAULT_CAPABILITIES = BitUtils.packBitList(
- NET_CAPABILITY_NOT_RESTRICTED,
- NET_CAPABILITY_TRUSTED,
- NET_CAPABILITY_NOT_VPN);
+ private static final long DEFAULT_CAPABILITIES =
+ (1L << NET_CAPABILITY_NOT_RESTRICTED) |
+ (1L << NET_CAPABILITY_TRUSTED) |
+ (1L << NET_CAPABILITY_NOT_VPN);
/**
* Capabilities that are managed by ConnectivityService.
@@ -784,11 +795,10 @@
*/
@VisibleForTesting
public static final long CONNECTIVITY_MANAGED_CAPABILITIES =
- BitUtils.packBitList(
- NET_CAPABILITY_VALIDATED,
- NET_CAPABILITY_CAPTIVE_PORTAL,
- NET_CAPABILITY_FOREGROUND,
- NET_CAPABILITY_PARTIAL_CONNECTIVITY);
+ (1L << NET_CAPABILITY_VALIDATED) |
+ (1L << NET_CAPABILITY_CAPTIVE_PORTAL) |
+ (1L << NET_CAPABILITY_FOREGROUND) |
+ (1L << NET_CAPABILITY_PARTIAL_CONNECTIVITY);
/**
* Capabilities that are allowed for all test networks. This list must be set so that it is safe
@@ -797,15 +807,14 @@
* IMS, SUPL, etc.
*/
private static final long TEST_NETWORKS_ALLOWED_CAPABILITIES =
- BitUtils.packBitList(
- NET_CAPABILITY_NOT_METERED,
- NET_CAPABILITY_TEMPORARILY_NOT_METERED,
- NET_CAPABILITY_NOT_RESTRICTED,
- NET_CAPABILITY_NOT_VPN,
- NET_CAPABILITY_NOT_ROAMING,
- NET_CAPABILITY_NOT_CONGESTED,
- NET_CAPABILITY_NOT_SUSPENDED,
- NET_CAPABILITY_NOT_VCN_MANAGED);
+ (1L << NET_CAPABILITY_NOT_METERED) |
+ (1L << NET_CAPABILITY_TEMPORARILY_NOT_METERED) |
+ (1L << NET_CAPABILITY_NOT_RESTRICTED) |
+ (1L << NET_CAPABILITY_NOT_VPN) |
+ (1L << NET_CAPABILITY_NOT_ROAMING) |
+ (1L << NET_CAPABILITY_NOT_CONGESTED) |
+ (1L << NET_CAPABILITY_NOT_SUSPENDED) |
+ (1L << NET_CAPABILITY_NOT_VCN_MANAGED);
/**
* Extra allowed capabilities for test networks that do not have TRANSPORT_CELLULAR. Test
@@ -813,7 +822,9 @@
* the risk of being used by running apps.
*/
private static final long TEST_NETWORKS_EXTRA_ALLOWED_CAPABILITIES_ON_NON_CELL =
- BitUtils.packBitList(NET_CAPABILITY_CBS, NET_CAPABILITY_DUN, NET_CAPABILITY_RCS);
+ (1L << NET_CAPABILITY_CBS) |
+ (1L << NET_CAPABILITY_DUN) |
+ (1L << NET_CAPABILITY_RCS);
/**
* Adds the given capability to this {@code NetworkCapability} instance.
@@ -1163,7 +1174,7 @@
* @hide
*/
public void maybeMarkCapabilitiesRestricted() {
- if (NetworkCapabilitiesUtils.inferRestrictedCapability(this)) {
+ if (NetworkCapabilitiesUtils.inferRestrictedCapability(mNetworkCapabilities)) {
removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
}
}
@@ -1257,6 +1268,7 @@
TRANSPORT_TEST,
TRANSPORT_USB,
TRANSPORT_THREAD,
+ TRANSPORT_SATELLITE,
})
public @interface Transport { }
@@ -1313,10 +1325,16 @@
*/
public static final int TRANSPORT_THREAD = 9;
+ /**
+ * Indicates this network uses a Satellite transport.
+ */
+ @FlaggedApi(Flags.SUPPORT_TRANSPORT_SATELLITE)
+ public static final int TRANSPORT_SATELLITE = 10;
+
/** @hide */
public static final int MIN_TRANSPORT = TRANSPORT_CELLULAR;
/** @hide */
- public static final int MAX_TRANSPORT = TRANSPORT_THREAD;
+ public static final int MAX_TRANSPORT = TRANSPORT_SATELLITE;
private static final int ALL_VALID_TRANSPORTS;
static {
@@ -1343,18 +1361,18 @@
"TEST",
"USB",
"THREAD",
+ "SATELLITE",
};
/**
* Allowed transports on an unrestricted test network (in addition to TRANSPORT_TEST).
*/
private static final long UNRESTRICTED_TEST_NETWORKS_ALLOWED_TRANSPORTS =
- BitUtils.packBitList(
- TRANSPORT_TEST,
- // Test eth networks are created with EthernetManager#setIncludeTestInterfaces
- TRANSPORT_ETHERNET,
- // Test VPN networks can be created but their UID ranges must be empty.
- TRANSPORT_VPN);
+ (1L << TRANSPORT_TEST) |
+ // Test eth networks are created with EthernetManager#setIncludeTestInterfaces
+ (1L << TRANSPORT_ETHERNET) |
+ // Test VPN networks can be created but their UID ranges must be empty.
+ (1L << TRANSPORT_VPN);
/**
* Adds the given transport type to this {@code NetworkCapability} instance.
@@ -1751,9 +1769,12 @@
public @NonNull NetworkCapabilities setNetworkSpecifier(
@NonNull NetworkSpecifier networkSpecifier) {
if (networkSpecifier != null
- // Transport can be test, or test + a single other transport
+ // Transport can be test, or test + a single other transport or cellular + satellite
+ // transport. Note: cellular + satellite combination is allowed since both transport
+ // use the same specifier, TelephonyNetworkSpecifier.
&& mTransportTypes != (1L << TRANSPORT_TEST)
- && Long.bitCount(mTransportTypes & ~(1L << TRANSPORT_TEST)) != 1) {
+ && Long.bitCount(mTransportTypes & ~(1L << TRANSPORT_TEST)) != 1
+ && !specifierAcceptableForMultipleTransports(mTransportTypes)) {
throw new IllegalStateException("Must have a single non-test transport specified to "
+ "use setNetworkSpecifier");
}
@@ -1763,6 +1784,12 @@
return this;
}
+ private boolean specifierAcceptableForMultipleTransports(long transportTypes) {
+ return (transportTypes & ~(1L << TRANSPORT_TEST))
+ // Cellular and satellite use the same NetworkSpecifier.
+ == (1 << TRANSPORT_CELLULAR | 1 << TRANSPORT_SATELLITE);
+ }
+
/**
* Sets the optional transport specific information.
*
@@ -2794,10 +2821,9 @@
* receiver holds the NETWORK_FACTORY permission. In all other cases, it will be the empty set.
*
* @return
- * @hide
*/
@NonNull
- @SystemApi
+ @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
public Set<Integer> getSubscriptionIds() {
return new ArraySet<>(mSubIds);
}
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 653e41d..f7600b2 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -20,7 +20,6 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -34,14 +33,13 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
-// TODO : replace with android.net.flags.Flags when aconfig is supported on udc-mainline-prod
-// import android.net.NetworkCapabilities.Flags;
import android.net.NetworkCapabilities.NetCapability;
import android.net.NetworkCapabilities.Transport;
import android.os.Build;
@@ -145,6 +143,12 @@
* Look up the specific capability to learn whether its usage requires this self-certification.
*/
public class NetworkRequest implements Parcelable {
+
+ /** @hide */
+ public static class Flags {
+ static final String REQUEST_RESTRICTED_WIFI =
+ "com.android.net.flags.request_restricted_wifi";
+ }
/**
* The first requestId value that will be allocated.
* @hide only used by ConnectivityService.
@@ -284,18 +288,6 @@
NET_CAPABILITY_TRUSTED,
NET_CAPABILITY_VALIDATED);
- /**
- * Capabilities that are forbidden by default.
- * Forbidden capabilities only make sense in NetworkRequest, not for network agents.
- * Therefore these capabilities are only in NetworkRequest.
- */
- private static final int[] DEFAULT_FORBIDDEN_CAPABILITIES = new int[] {
- // TODO(b/313030307): this should contain NET_CAPABILITY_LOCAL_NETWORK.
- // We cannot currently add it because doing so would crash if the module rolls back,
- // because JobScheduler persists NetworkRequests to disk, and existing production code
- // does not consider LOCAL_NETWORK to be a valid capability.
- };
-
private final NetworkCapabilities mNetworkCapabilities;
// A boolean that represents whether the NOT_VCN_MANAGED capability should be deduced when
@@ -311,16 +303,6 @@
// it for apps that do not have the NETWORK_SETTINGS permission.
mNetworkCapabilities = new NetworkCapabilities();
mNetworkCapabilities.setSingleUid(Process.myUid());
- // Default forbidden capabilities are foremost meant to help with backward
- // compatibility. When adding new types of network identified by a capability that
- // might confuse older apps, a default forbidden capability will have apps not see
- // these networks unless they explicitly ask for it.
- // If the app called clearCapabilities() it will see everything, but then it
- // can be argued that it's fair to send them too, since it asked for everything
- // explicitly.
- for (final int forbiddenCap : DEFAULT_FORBIDDEN_CAPABILITIES) {
- mNetworkCapabilities.addForbiddenCapability(forbiddenCap);
- }
}
/**
@@ -630,10 +612,9 @@
* NETWORK_FACTORY permission.
*
* @param subIds A {@code Set} that represents subscription IDs.
- * @hide
*/
@NonNull
- @SystemApi
+ @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
public Builder setSubscriptionIds(@NonNull Set<Integer> subIds) {
mNetworkCapabilities.setSubscriptionIds(subIds);
return this;
@@ -890,4 +871,17 @@
// a new array.
return networkCapabilities.getTransportTypes();
}
+
+ /**
+ * Gets all the subscription ids set on this {@code NetworkRequest} instance.
+ *
+ * @return Set of Integer values for this instance.
+ */
+ @NonNull
+ @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
+ public Set<Integer> getSubscriptionIds() {
+ // No need to make a defensive copy here as NC#getSubscriptionIds() already returns
+ // a new set.
+ return networkCapabilities.getSubscriptionIds();
+ }
}
diff --git a/framework/src/android/net/NetworkStackBpfNetMaps.java b/framework/src/android/net/NetworkStackBpfNetMaps.java
new file mode 100644
index 0000000..b7c4e34
--- /dev/null
+++ b/framework/src/android/net/NetworkStackBpfNetMaps.java
@@ -0,0 +1,187 @@
+/*
+ * 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;
+
+import static android.net.BpfNetMapsConstants.CONFIGURATION_MAP_PATH;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
+import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.os.Build;
+import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct.S32;
+import com.android.net.module.util.Struct.U32;
+import com.android.net.module.util.Struct.U8;
+
+/**
+ * A helper class to *read* java BpfMaps for network stack.
+ * BpfMap operations that are not used from network stack should be in
+ * {@link com.android.server.BpfNetMaps}
+ * @hide
+ */
+// NetworkStack can not use this before U due to b/326143935
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+public class NetworkStackBpfNetMaps {
+ private static final String TAG = NetworkStackBpfNetMaps.class.getSimpleName();
+
+ // Locally store the handle of bpf maps. The FileDescriptors are statically cached inside the
+ // BpfMap implementation.
+
+ // Bpf map to store various networking configurations, the format of the value is different
+ // for different keys. See BpfNetMapsConstants#*_CONFIGURATION_KEY for keys.
+ private final IBpfMap<S32, U32> mConfigurationMap;
+ // Bpf map to store per uid traffic control configurations.
+ // See {@link UidOwnerValue} for more detail.
+ private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap;
+ private final IBpfMap<S32, U8> mDataSaverEnabledMap;
+ private final Dependencies mDeps;
+
+ private static class SingletonHolder {
+ static final NetworkStackBpfNetMaps sInstance = new NetworkStackBpfNetMaps();
+ }
+
+ @NonNull
+ public static NetworkStackBpfNetMaps getInstance() {
+ return SingletonHolder.sInstance;
+ }
+
+ private NetworkStackBpfNetMaps() {
+ this(new Dependencies());
+ }
+
+ // While the production code uses the singleton to optimize for performance and deal with
+ // concurrent access, the test needs to use a non-static approach for dependency injection and
+ // mocking virtual bpf maps.
+ @VisibleForTesting
+ public NetworkStackBpfNetMaps(@NonNull Dependencies deps) {
+ if (!SdkLevel.isAtLeastT()) {
+ throw new UnsupportedOperationException(
+ NetworkStackBpfNetMaps.class.getSimpleName()
+ + " is not supported below Android T");
+ }
+ mDeps = deps;
+ mConfigurationMap = mDeps.getConfigurationMap();
+ mUidOwnerMap = mDeps.getUidOwnerMap();
+ mDataSaverEnabledMap = mDeps.getDataSaverEnabledMap();
+ }
+
+ /**
+ * Dependencies of BpfNetMapReader, for injection in tests.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ /** Get the configuration map. */
+ public IBpfMap<S32, U32> getConfigurationMap() {
+ try {
+ return new BpfMap<>(CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDONLY,
+ S32.class, U32.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open configuration map", e);
+ }
+ }
+
+ /** Get the uid owner map. */
+ public IBpfMap<S32, UidOwnerValue> getUidOwnerMap() {
+ try {
+ return new BpfMap<>(UID_OWNER_MAP_PATH, BpfMap.BPF_F_RDONLY,
+ S32.class, UidOwnerValue.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open uid owner map", e);
+ }
+ }
+
+ /** Get the data saver enabled map. */
+ public IBpfMap<S32, U8> getDataSaverEnabledMap() {
+ try {
+ return new BpfMap<>(DATA_SAVER_ENABLED_MAP_PATH, BpfMap.BPF_F_RDONLY, S32.class,
+ U8.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open data saver enabled map", e);
+ }
+ }
+ }
+
+ /**
+ * Get the specified firewall chain's status.
+ *
+ * @param chain target chain
+ * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public boolean isChainEnabled(final int chain) {
+ return BpfNetMapsUtils.isChainEnabled(mConfigurationMap, chain);
+ }
+
+ /**
+ * Get firewall rule of specified firewall chain on specified uid.
+ *
+ * @param chain target chain
+ * @param uid target uid
+ * @return either {@link ConnectivityManager#FIREWALL_RULE_ALLOW} or
+ * {@link ConnectivityManager#FIREWALL_RULE_DENY}.
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public int getUidRule(final int chain, final int uid) {
+ return BpfNetMapsUtils.getUidRule(mUidOwnerMap, chain, uid);
+ }
+
+ /**
+ * Return whether the network is blocked by firewall chains for the given uid.
+ *
+ * Note that {@link #getDataSaverEnabled()} has a latency before V.
+ *
+ * @param uid The target uid.
+ * @param isNetworkMetered Whether the target network is metered.
+ *
+ * @return True if the network is blocked. Otherwise, false.
+ * @throws ServiceSpecificException if the read fails.
+ *
+ * @hide
+ */
+ public boolean isUidNetworkingBlocked(final int uid, boolean isNetworkMetered) {
+ return BpfNetMapsUtils.isUidNetworkingBlocked(uid, isNetworkMetered,
+ mConfigurationMap, mUidOwnerMap, mDataSaverEnabledMap);
+ }
+
+ /**
+ * Get Data Saver enabled or disabled
+ *
+ * Note that before V, the data saver status in bpf is written by ConnectivityService
+ * when receiving {@link ConnectivityManager#ACTION_RESTRICT_BACKGROUND_CHANGED}. Thus,
+ * the status is not synchronized.
+ * On V+, the data saver status is set by platform code when enabling/disabling
+ * data saver, which is synchronized.
+ *
+ * @return whether Data Saver is enabled or disabled.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public boolean getDataSaverEnabled() {
+ return BpfNetMapsUtils.getDataSaverEnabled(mDataSaverEnabledMap);
+ }
+}
diff --git a/framework/src/android/net/NetworkUtils.java b/framework/src/android/net/NetworkUtils.java
index fbdc024..18feb84 100644
--- a/framework/src/android/net/NetworkUtils.java
+++ b/framework/src/android/net/NetworkUtils.java
@@ -438,4 +438,9 @@
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();
+
+ /** Returns whether the Linux Kernel is x86 */
+ public static native boolean isKernelX86();
}
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index dfe5867..a80db85 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -84,6 +84,21 @@
@ChangeId
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
public static final long ENABLE_PLATFORM_MDNS_BACKEND = 270306772L;
+
+ /**
+ * Apps targeting Android V or higher receive network callbacks from local networks as default
+ *
+ * Apps targeting lower than {@link android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM} need
+ * to add {@link android.net.NetworkCapabilities#NET_CAPABILITY_LOCAL_NETWORK} to the
+ * {@link android.net.NetworkCapabilities} of the {@link android.net.NetworkRequest} to receive
+ * {@link android.net.ConnectivityManager.NetworkCallback} from local networks.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public static final long ENABLE_MATCH_LOCAL_NETWORK = 319212206L;
+
private ConnectivityCompatChanges() {
}
}
diff --git a/nearby/apex/Android.bp b/nearby/apex/Android.bp
index d7f063a..5fdf5c9 100644
--- a/nearby/apex/Android.bp
+++ b/nearby/apex/Android.bp
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/nearby/framework/Android.bp b/nearby/framework/Android.bp
index 4bb9efd..4be102c 100644
--- a/nearby/framework/Android.bp
+++ b/nearby/framework/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -49,6 +50,7 @@
"androidx.annotation_annotation",
"framework-annotations-lib",
"framework-bluetooth",
+ "framework-location.stubs.module_lib",
],
static_libs: [
"modules-utils-preconditions",
diff --git a/nearby/framework/java/android/nearby/INearbyManager.aidl b/nearby/framework/java/android/nearby/INearbyManager.aidl
index 7af271e..21ae0ac 100644
--- a/nearby/framework/java/android/nearby/INearbyManager.aidl
+++ b/nearby/framework/java/android/nearby/INearbyManager.aidl
@@ -20,6 +20,7 @@
import android.nearby.IScanListener;
import android.nearby.BroadcastRequestParcelable;
import android.nearby.ScanRequest;
+import android.nearby.PoweredOffFindingEphemeralId;
import android.nearby.aidl.IOffloadCallback;
/**
@@ -40,4 +41,10 @@
void stopBroadcast(in IBroadcastListener callback, String packageName, @nullable String attributionTag);
void queryOffloadCapability(in IOffloadCallback callback) ;
-}
\ No newline at end of file
+
+ void setPoweredOffFindingEphemeralIds(in List<PoweredOffFindingEphemeralId> eids);
+
+ void setPoweredOffModeEnabled(boolean enabled);
+
+ boolean getPoweredOffModeEnabled();
+}
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index 00f1c38..cae653d 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -18,6 +18,7 @@
import android.Manifest;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -25,9 +26,12 @@
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.bluetooth.BluetoothManager;
import android.content.Context;
+import android.location.LocationManager;
import android.nearby.aidl.IOffloadCallback;
import android.os.RemoteException;
+import android.os.SystemProperties;
import android.provider.Settings;
import android.util.Log;
@@ -37,6 +41,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
+import java.util.List;
import java.util.Objects;
import java.util.WeakHashMap;
import java.util.concurrent.Executor;
@@ -75,8 +80,51 @@
int ERROR = 2;
}
+ /**
+ * Return value of {@link #getPoweredOffFindingMode()} when this powered off finding is not
+ * supported the device.
+ */
+ @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ public static final int POWERED_OFF_FINDING_MODE_UNSUPPORTED = 0;
+
+ /**
+ * Return value of {@link #getPoweredOffFindingMode()} and argument of {@link
+ * #setPoweredOffFindingMode(int)} when powered off finding is supported but disabled. The
+ * device will not start to advertise when powered off.
+ */
+ @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ public static final int POWERED_OFF_FINDING_MODE_DISABLED = 1;
+
+ /**
+ * Return value of {@link #getPoweredOffFindingMode()} and argument of {@link
+ * #setPoweredOffFindingMode(int)} when powered off finding is enabled. The device will start to
+ * advertise when powered off.
+ */
+ @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ public static final int POWERED_OFF_FINDING_MODE_ENABLED = 2;
+
+ /**
+ * Powered off finding modes.
+ *
+ * @hide
+ */
+ @IntDef(
+ prefix = {"POWERED_OFF_FINDING_MODE"},
+ value = {
+ POWERED_OFF_FINDING_MODE_UNSUPPORTED,
+ POWERED_OFF_FINDING_MODE_DISABLED,
+ POWERED_OFF_FINDING_MODE_ENABLED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PoweredOffFindingMode {}
+
private static final String TAG = "NearbyManager";
+ private static final int POWERED_OFF_FINDING_EID_LENGTH = 20;
+
+ private static final String POWER_OFF_FINDING_SUPPORTED_PROPERTY =
+ "ro.bluetooth.finder.supported";
+
/**
* TODO(b/286137024): Remove this when CTS R5 is rolled out.
* Whether allows Fast Pair to scan.
@@ -456,4 +504,124 @@
"successfully %s Fast Pair scan", enable ? "enables" : "disables"));
}
+ /**
+ * Sets the precomputed EIDs for advertising when the phone is powered off. The Bluetooth
+ * controller will store these EIDs in its memory, and will start advertising them in Find My
+ * Device network EID frames when powered off, only if the powered off finding mode was
+ * previously enabled by calling {@link #setPoweredOffFindingMode(int)}.
+ *
+ * <p>The EIDs are cryptographic ephemeral identifiers that change periodically, based on the
+ * Android clock at the time of the shutdown. They are used as the public part of asymmetric key
+ * pairs. Members of the Find My Device network can use them to encrypt the location of where
+ * they sight the advertising device. Only someone in possession of the private key (the device
+ * owner or someone that the device owner shared the key with) can decrypt this encrypted
+ * location.
+ *
+ * <p>Android will typically call this method during the shutdown process. Even after the
+ * method was called, it is still possible to call {#link setPoweredOffFindingMode() to disable
+ * the advertisement, for example to temporarily disable it for a single shutdown.
+ *
+ * <p>If called more than once, the EIDs of the most recent call overrides the EIDs from any
+ * previous call.
+ *
+ * @throws IllegalArgumentException if the length of one of the EIDs is not 20 bytes
+ */
+ @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void setPoweredOffFindingEphemeralIds(@NonNull List<byte[]> eids) {
+ Objects.requireNonNull(eids);
+ if (!isPoweredOffFindingSupported()) {
+ throw new UnsupportedOperationException(
+ "Powered off finding is not supported on this device");
+ }
+ List<PoweredOffFindingEphemeralId> ephemeralIdList = eids.stream().map(
+ eid -> {
+ Preconditions.checkArgument(eid.length == POWERED_OFF_FINDING_EID_LENGTH);
+ PoweredOffFindingEphemeralId ephemeralId = new PoweredOffFindingEphemeralId();
+ ephemeralId.bytes = eid;
+ return ephemeralId;
+ }).toList();
+ try {
+ mService.setPoweredOffFindingEphemeralIds(ephemeralIdList);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ }
+
+ /**
+ * Turns the powered off finding on or off. Power off finding will operate only if this method
+ * was called at least once since boot, and the value of the argument {@code
+ * poweredOffFindinMode} was {@link #POWERED_OFF_FINDING_MODE_ENABLED} the last time the method
+ * was called.
+ *
+ * <p>When an Android device with the powered off finding feature is turned off (either as part
+ * of a normal shutdown or due to dead battery), its Bluetooth chip starts to advertise Find My
+ * Device network EID frames with the EID payload that were provided by the last call to {@link
+ * #setPoweredOffFindingEphemeralIds(List)}. These EIDs can be sighted by other Android devices
+ * in BLE range that are part of the Find My Device network. The Android sighters use the EID to
+ * encrypt the location of the Android device and upload it to the server, in a way that only
+ * the owner of the advertising device, or people that the owner shared their encryption key
+ * with, can decrypt the location.
+ *
+ * @param poweredOffFindingMode {@link #POWERED_OFF_FINDING_MODE_ENABLED} or {@link
+ * #POWERED_OFF_FINDING_MODE_DISABLED}
+ *
+ * @throws IllegalStateException if called with {@link #POWERED_OFF_FINDING_MODE_ENABLED} when
+ * Bluetooth or location services are disabled
+ */
+ @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void setPoweredOffFindingMode(@PoweredOffFindingMode int poweredOffFindingMode) {
+ Preconditions.checkArgument(
+ poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED
+ || poweredOffFindingMode == POWERED_OFF_FINDING_MODE_DISABLED,
+ "invalid poweredOffFindingMode");
+ if (!isPoweredOffFindingSupported()) {
+ throw new UnsupportedOperationException(
+ "Powered off finding is not supported on this device");
+ }
+ if (poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED) {
+ Preconditions.checkState(areLocationAndBluetoothEnabled(),
+ "Location services and Bluetooth must be on");
+ }
+ try {
+ mService.setPoweredOffModeEnabled(
+ poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the state of the powered off finding feature.
+ *
+ * <p>{@link #POWERED_OFF_FINDING_MODE_UNSUPPORTED} if the feature is not supported by the
+ * device, {@link #POWERED_OFF_FINDING_MODE_DISABLED} if this was the last value set by {@link
+ * #setPoweredOffFindingMode(int)} or if no value was set since boot, {@link
+ * #POWERED_OFF_FINDING_MODE_ENABLED} if this was the last value set by {@link
+ * #setPoweredOffFindingMode(int)}
+ */
+ @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public @PoweredOffFindingMode int getPoweredOffFindingMode() {
+ if (!isPoweredOffFindingSupported()) {
+ return POWERED_OFF_FINDING_MODE_UNSUPPORTED;
+ }
+ try {
+ return mService.getPoweredOffModeEnabled()
+ ? POWERED_OFF_FINDING_MODE_ENABLED : POWERED_OFF_FINDING_MODE_DISABLED;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private boolean isPoweredOffFindingSupported() {
+ return Boolean.parseBoolean(SystemProperties.get(POWER_OFF_FINDING_SUPPORTED_PROPERTY));
+ }
+
+ private boolean areLocationAndBluetoothEnabled() {
+ return mContext.getSystemService(BluetoothManager.class).getAdapter().isEnabled()
+ && mContext.getSystemService(LocationManager.class).isLocationEnabled();
+ }
}
diff --git a/nearby/framework/java/android/nearby/PoweredOffFindingEphemeralId.aidl b/nearby/framework/java/android/nearby/PoweredOffFindingEphemeralId.aidl
new file mode 100644
index 0000000..9f4bfef
--- /dev/null
+++ b/nearby/framework/java/android/nearby/PoweredOffFindingEphemeralId.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+/**
+ * Find My Device network ephemeral ID for powered off finding.
+ *
+ * @hide
+ */
+parcelable PoweredOffFindingEphemeralId {
+ byte[20] bytes;
+}
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index 17b80b0..749113d 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -42,6 +43,7 @@
],
static_libs: [
"androidx.core_core",
+ "android.hardware.bluetooth.finder-V1-java",
"guava",
"libprotobuf-java-lite",
"modules-utils-build",
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 3c183ec..1575f07 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -35,12 +35,14 @@
import android.nearby.INearbyManager;
import android.nearby.IScanListener;
import android.nearby.NearbyManager;
+import android.nearby.PoweredOffFindingEphemeralId;
import android.nearby.ScanRequest;
import android.nearby.aidl.IOffloadCallback;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.managers.BluetoothFinderManager;
import com.android.server.nearby.managers.BroadcastProviderManager;
import com.android.server.nearby.managers.DiscoveryManager;
import com.android.server.nearby.managers.DiscoveryProviderManager;
@@ -50,6 +52,8 @@
import com.android.server.nearby.util.permissions.BroadcastPermissions;
import com.android.server.nearby.util.permissions.DiscoveryPermissions;
+import java.util.List;
+
/** Service implementing nearby functionality. */
public class NearbyService extends INearbyManager.Stub {
public static final String TAG = "NearbyService";
@@ -79,6 +83,7 @@
};
private final DiscoveryManager mDiscoveryProviderManager;
private final BroadcastProviderManager mBroadcastProviderManager;
+ private final BluetoothFinderManager mBluetoothFinderManager;
public NearbyService(Context context) {
mContext = context;
@@ -90,6 +95,7 @@
mNearbyConfiguration.refactorDiscoveryManager()
? new DiscoveryProviderManager(context, mInjector)
: new DiscoveryProviderManagerLegacy(context, mInjector);
+ mBluetoothFinderManager = new BluetoothFinderManager();
}
@VisibleForTesting
@@ -148,6 +154,30 @@
mDiscoveryProviderManager.queryOffloadCapability(callback);
}
+ @Override
+ public void setPoweredOffFindingEphemeralIds(List<PoweredOffFindingEphemeralId> eids) {
+ // Permissions check
+ enforceBluetoothPrivilegedPermission(mContext);
+
+ mBluetoothFinderManager.sendEids(eids);
+ }
+
+ @Override
+ public void setPoweredOffModeEnabled(boolean enabled) {
+ // Permissions check
+ enforceBluetoothPrivilegedPermission(mContext);
+
+ mBluetoothFinderManager.setPoweredOffFinderMode(enabled);
+ }
+
+ @Override
+ public boolean getPoweredOffModeEnabled() {
+ // Permissions check
+ enforceBluetoothPrivilegedPermission(mContext);
+
+ return mBluetoothFinderManager.getPoweredOffFinderMode();
+ }
+
/**
* Called by the service initializer.
*
diff --git a/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java b/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java
new file mode 100644
index 0000000..365b099
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java
@@ -0,0 +1,166 @@
+/*
+ * 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.nearby.managers;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.TargetApi;
+import android.hardware.bluetooth.finder.Eid;
+import android.hardware.bluetooth.finder.IBluetoothFinder;
+import android.nearby.PoweredOffFindingEphemeralId;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+
+import java.util.List;
+
+/** Connects to {@link IBluetoothFinder} HAL and invokes its API. */
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
+public class BluetoothFinderManager {
+
+ private static final String HAL_INSTANCE_NAME = IBluetoothFinder.DESCRIPTOR + "/default";
+
+ private IBluetoothFinder mBluetoothFinder;
+ private IBinder.DeathRecipient mServiceDeathRecipient;
+ private final Object mLock = new Object();
+
+ private boolean initBluetoothFinderHal() {
+ final String methodStr = "initBluetoothFinderHal";
+ if (!SdkLevel.isAtLeastV()) return false;
+ synchronized (mLock) {
+ if (mBluetoothFinder != null) {
+ Log.i(TAG, "Bluetooth Finder HAL is already initialized");
+ return true;
+ }
+ try {
+ mBluetoothFinder = getServiceMockable();
+ if (mBluetoothFinder == null) {
+ Log.e(TAG, "Unable to obtain IBluetoothFinder");
+ return false;
+ }
+ Log.i(TAG, "Obtained IBluetoothFinder. Local ver: " + IBluetoothFinder.VERSION
+ + ", Remote ver: " + mBluetoothFinder.getInterfaceVersion());
+
+ IBinder serviceBinder = getServiceBinderMockable();
+ if (serviceBinder == null) {
+ Log.e(TAG, "Unable to obtain the service binder for IBluetoothFinder");
+ return false;
+ }
+ mServiceDeathRecipient = new BluetoothFinderDeathRecipient();
+ serviceBinder.linkToDeath(mServiceDeathRecipient, /* flags= */ 0);
+
+ Log.i(TAG, "Bluetooth Finder HAL initialization was successful");
+ return true;
+ } catch (RemoteException e) {
+ handleRemoteException(e, methodStr);
+ } catch (Exception e) {
+ Log.e(TAG, methodStr + " encountered an exception: " + e);
+ }
+ return false;
+ }
+ }
+
+ @VisibleForTesting
+ protected IBluetoothFinder getServiceMockable() {
+ return IBluetoothFinder.Stub.asInterface(
+ ServiceManager.waitForDeclaredService(HAL_INSTANCE_NAME));
+ }
+
+ @VisibleForTesting
+ protected IBinder getServiceBinderMockable() {
+ return mBluetoothFinder.asBinder();
+ }
+
+ private class BluetoothFinderDeathRecipient implements IBinder.DeathRecipient {
+ @Override
+ public void binderDied() {
+ Log.e(TAG, "BluetoothFinder service died.");
+ synchronized (mLock) {
+ mBluetoothFinder = null;
+ }
+ }
+ }
+
+ /** See comments for {@link IBluetoothFinder#sendEids(Eid[])} */
+ public void sendEids(List<PoweredOffFindingEphemeralId> eids) {
+ final String methodStr = "sendEids";
+ if (!checkHalAndLogFailure(methodStr)) return;
+ Eid[] eidArray = eids.stream().map(
+ ephmeralId -> {
+ Eid eid = new Eid();
+ eid.bytes = ephmeralId.bytes;
+ return eid;
+ }).toArray(Eid[]::new);
+ try {
+ mBluetoothFinder.sendEids(eidArray);
+ } catch (RemoteException e) {
+ handleRemoteException(e, methodStr);
+ } catch (ServiceSpecificException e) {
+ handleServiceSpecificException(e, methodStr);
+ }
+ }
+
+ /** See comments for {@link IBluetoothFinder#setPoweredOffFinderMode(boolean)} */
+ public void setPoweredOffFinderMode(boolean enable) {
+ final String methodStr = "setPoweredOffMode";
+ if (!checkHalAndLogFailure(methodStr)) return;
+ try {
+ mBluetoothFinder.setPoweredOffFinderMode(enable);
+ } catch (RemoteException e) {
+ handleRemoteException(e, methodStr);
+ } catch (ServiceSpecificException e) {
+ handleServiceSpecificException(e, methodStr);
+ }
+ }
+
+ /** See comments for {@link IBluetoothFinder#getPoweredOffFinderMode()} */
+ public boolean getPoweredOffFinderMode() {
+ final String methodStr = "getPoweredOffMode";
+ if (!checkHalAndLogFailure(methodStr)) return false;
+ try {
+ return mBluetoothFinder.getPoweredOffFinderMode();
+ } catch (RemoteException e) {
+ handleRemoteException(e, methodStr);
+ } catch (ServiceSpecificException e) {
+ handleServiceSpecificException(e, methodStr);
+ }
+ return false;
+ }
+
+ private boolean checkHalAndLogFailure(String methodStr) {
+ if ((mBluetoothFinder == null) && !initBluetoothFinderHal()) {
+ Log.e(TAG, "Unable to call " + methodStr + " because IBluetoothFinder is null.");
+ return false;
+ }
+ return true;
+ }
+
+ private void handleRemoteException(RemoteException e, String methodStr) {
+ mBluetoothFinder = null;
+ Log.e(TAG, methodStr + " failed with remote exception: " + e);
+ }
+
+ private void handleServiceSpecificException(ServiceSpecificException e, String methodStr) {
+ Log.e(TAG, methodStr + " failed with service-specific exception: " + e);
+ }
+}
diff --git a/nearby/service/lint-baseline.xml b/nearby/service/lint-baseline.xml
index a4761ab..3477594 100644
--- a/nearby/service/lint-baseline.xml
+++ b/nearby/service/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
<issue
id="NewApi"
@@ -8,7 +8,7 @@
errorLine2=" ~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java"
- line="263"
+ line="289"
column="54"/>
</issue>
diff --git a/nearby/service/proto/Android.bp b/nearby/service/proto/Android.bp
index 1b00cf6..be5a0b3 100644
--- a/nearby/service/proto/Android.bp
+++ b/nearby/service/proto/Android.bp
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -41,4 +42,4 @@
apex_available: [
"com.android.tethering",
],
-}
\ No newline at end of file
+}
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 4309d7e..8009303 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -33,6 +34,7 @@
"framework-bluetooth.stubs.module_lib",
"framework-configinfrastructure",
"framework-connectivity-t.impl",
+ "framework-location.stubs.module_lib",
],
srcs: ["src/**/*.java"],
test_suites: [
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index bc9691d..832ac03 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -25,12 +25,14 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
import android.app.UiAutomation;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.bluetooth.cts.BTAdapterUtils;
import android.content.Context;
+import android.location.LocationManager;
import android.nearby.BroadcastCallback;
import android.nearby.BroadcastRequest;
import android.nearby.NearbyDevice;
@@ -42,6 +44,8 @@
import android.nearby.ScanCallback;
import android.nearby.ScanRequest;
import android.os.Build;
+import android.os.Process;
+import android.os.UserHandle;
import android.provider.DeviceConfig;
import androidx.annotation.NonNull;
@@ -50,6 +54,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
+import com.android.compatibility.common.util.SystemUtil;
import com.android.modules.utils.build.SdkLevel;
import org.junit.Before;
@@ -57,6 +62,7 @@
import org.junit.runner.RunWith;
import java.util.Collections;
+import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -189,6 +195,92 @@
mScanCallback.onError(ERROR_UNSUPPORTED);
}
+ @Test
+ public void testsetPoweredOffFindingEphemeralIds() {
+ // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+ assumeTrue(SdkLevel.isAtLeastV());
+ // Only test supporting devices.
+ if (mNearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+ mNearbyManager.setPoweredOffFindingEphemeralIds(List.of(new byte[20], new byte[20]));
+ }
+
+ @Test
+ public void testsetPoweredOffFindingEphemeralIds_noPrivilegedPermission() {
+ // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+ assumeTrue(SdkLevel.isAtLeastV());
+ // Only test supporting devices.
+ if (mNearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+ mUiAutomation.dropShellPermissionIdentity();
+
+ assertThrows(SecurityException.class,
+ () -> mNearbyManager.setPoweredOffFindingEphemeralIds(List.of(new byte[20])));
+ }
+
+
+ @Test
+ public void testSetAndGetPoweredOffFindingMode_enabled() {
+ // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+ assumeTrue(SdkLevel.isAtLeastV());
+ // Only test supporting devices.
+ if (mNearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+ enableLocation();
+ // enableLocation() has dropped shell permission identity.
+ mUiAutomation.adoptShellPermissionIdentity(BLUETOOTH_PRIVILEGED);
+
+ mNearbyManager.setPoweredOffFindingMode(
+ NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
+ assertThat(mNearbyManager.getPoweredOffFindingMode())
+ .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
+ }
+
+ @Test
+ public void testSetAndGetPoweredOffFindingMode_disabled() {
+ // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+ assumeTrue(SdkLevel.isAtLeastV());
+ // Only test supporting devices.
+ if (mNearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+ mNearbyManager.setPoweredOffFindingMode(
+ NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
+ assertThat(mNearbyManager.getPoweredOffFindingMode())
+ .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
+ }
+
+ @Test
+ public void testSetPoweredOffFindingMode_noPrivilegedPermission() {
+ // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+ assumeTrue(SdkLevel.isAtLeastV());
+ // Only test supporting devices.
+ if (mNearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+ enableLocation();
+ mUiAutomation.dropShellPermissionIdentity();
+
+ assertThrows(SecurityException.class, () -> mNearbyManager
+ .setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED));
+ }
+
+ @Test
+ public void testGetPoweredOffFindingMode_noPrivilegedPermission() {
+ // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+ assumeTrue(SdkLevel.isAtLeastV());
+ // Only test supporting devices.
+ if (mNearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+ mUiAutomation.dropShellPermissionIdentity();
+
+ assertThrows(SecurityException.class, () -> mNearbyManager.getPoweredOffFindingMode());
+ }
+
private void enableBluetooth() {
BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
BluetoothAdapter bluetoothAdapter = manager.getAdapter();
@@ -197,6 +289,13 @@
}
}
+ private void enableLocation() {
+ LocationManager locationManager = mContext.getSystemService(LocationManager.class);
+ UserHandle user = Process.myUserHandle();
+ SystemUtil.runWithShellPermissionIdentity(
+ mUiAutomation, () -> locationManager.setLocationEnabledForUser(true, user));
+ }
+
private static class OffloadCallback implements Consumer<OffloadCapability> {
@Override
public void accept(OffloadCapability aBoolean) {
diff --git a/nearby/tests/integration/privileged/Android.bp b/nearby/tests/integration/privileged/Android.bp
index 9b6e488..5e64009 100644
--- a/nearby/tests/integration/privileged/Android.bp
+++ b/nearby/tests/integration/privileged/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
index 506b4e2..b949720 100644
--- a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
+++ b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
@@ -29,6 +29,7 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -96,4 +97,49 @@
)
nearbyManager.stopBroadcast(broadcastCallback)
}
+
+ /** Verify privileged app can set powered off finding ephemeral IDs without exception. */
+ @Test
+ fun testNearbyManagerSetPoweredOffFindingEphemeralIds_fromPrivilegedApp_succeed() {
+ val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+ // Only test supporting devices.
+ if (nearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return
+
+ val eid = ByteArray(20)
+
+ nearbyManager.setPoweredOffFindingEphemeralIds(listOf(eid))
+ }
+
+ /**
+ * Verifies that [NearbyManager.setPoweredOffFindingEphemeralIds] checkes the ephemeral ID
+ * length.
+ */
+ @Test
+ fun testNearbyManagerSetPoweredOffFindingEphemeralIds_wrongSize_throwsException() {
+ val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+ // Only test supporting devices.
+ if (nearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return
+
+ assertThrows(IllegalArgumentException::class.java) {
+ nearbyManager.setPoweredOffFindingEphemeralIds(listOf(ByteArray(21)))
+ }
+ assertThrows(IllegalArgumentException::class.java) {
+ nearbyManager.setPoweredOffFindingEphemeralIds(listOf(ByteArray(19)))
+ }
+ }
+
+ /** Verify privileged app can set and get powered off finding mode without exception. */
+ @Test
+ fun testNearbyManagerSetGetPoweredOffMode_fromPrivilegedApp_succeed() {
+ val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+ // Only test supporting devices.
+ if (nearbyManager.getPoweredOffFindingMode()
+ == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return
+
+ nearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED)
+ assertThat(nearbyManager.getPoweredOffFindingMode())
+ .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED)
+ }
}
diff --git a/nearby/tests/integration/untrusted/Android.bp b/nearby/tests/integration/untrusted/Android.bp
index 75f765b..e6259c5 100644
--- a/nearby/tests/integration/untrusted/Android.bp
+++ b/nearby/tests/integration/untrusted/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt b/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt
index 7bf9f63..015d022 100644
--- a/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt
+++ b/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt
@@ -30,12 +30,12 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.LogcatWaitMixin
import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import java.util.Calendar
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import java.time.Duration
-import java.util.Calendar
@RunWith(AndroidJUnit4::class)
class NearbyManagerTest {
@@ -151,6 +151,46 @@
).isTrue()
}
+ /**
+ * Verify untrusted app can't set powered off finding ephemeral IDs because it needs
+ * BLUETOOTH_PRIVILEGED permission which is not for use by third-party applications.
+ */
+ @Test
+ fun testNearbyManagerSetPoweredOffFindingEphemeralIds_fromUnTrustedApp_throwsException() {
+ val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+ val eid = ByteArray(20)
+
+ assertThrows(SecurityException::class.java) {
+ nearbyManager.setPoweredOffFindingEphemeralIds(listOf(eid))
+ }
+ }
+
+ /**
+ * Verify untrusted app can't set powered off finding mode because it needs BLUETOOTH_PRIVILEGED
+ * permission which is not for use by third-party applications.
+ */
+ @Test
+ fun testNearbyManagerSetPoweredOffFindingMode_fromUnTrustedApp_throwsException() {
+ val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+
+ assertThrows(SecurityException::class.java) {
+ nearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED)
+ }
+ }
+
+ /**
+ * Verify untrusted app can't get powered off finding mode because it needs BLUETOOTH_PRIVILEGED
+ * permission which is not for use by third-party applications.
+ */
+ @Test
+ fun testNearbyManagerGetPoweredOffFindingMode_fromUnTrustedApp_throwsException() {
+ val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+
+ assertThrows(SecurityException::class.java) {
+ nearbyManager.getPoweredOffFindingMode()
+ }
+ }
+
companion object {
private val WAIT_INVALID_OPERATIONS_LOGS_TIMEOUT = Duration.ofSeconds(5)
}
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
index bbf42c7..2950568 100644
--- a/nearby/tests/unit/Android.bp
+++ b/nearby/tests/unit/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
new file mode 100644
index 0000000..32286e1
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
@@ -0,0 +1,191 @@
+/*
+ * 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.nearby.managers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.bluetooth.finder.Eid;
+import android.hardware.bluetooth.finder.IBluetoothFinder;
+import android.nearby.PoweredOffFindingEphemeralId;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+
+import com.android.modules.utils.build.SdkLevel;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+public class BluetoothFinderManagerTest {
+ private BluetoothFinderManager mBluetoothFinderManager;
+ private boolean mGetServiceCalled = false;
+
+ @Mock private IBluetoothFinder mIBluetoothFinderMock;
+ @Mock private IBinder mServiceBinderMock;
+
+ private ArgumentCaptor<DeathRecipient> mDeathRecipientCaptor =
+ ArgumentCaptor.forClass(DeathRecipient.class);
+
+ private ArgumentCaptor<Eid[]> mEidArrayCaptor = ArgumentCaptor.forClass(Eid[].class);
+
+ private class BluetoothFinderManagerSpy extends BluetoothFinderManager {
+ @Override
+ protected IBluetoothFinder getServiceMockable() {
+ mGetServiceCalled = true;
+ return mIBluetoothFinderMock;
+ }
+
+ @Override
+ protected IBinder getServiceBinderMockable() {
+ return mServiceBinderMock;
+ }
+ }
+
+ @Before
+ public void setup() {
+ // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+ assumeTrue(SdkLevel.isAtLeastV());
+ MockitoAnnotations.initMocks(this);
+ mBluetoothFinderManager = new BluetoothFinderManagerSpy();
+ }
+
+ @Test
+ public void testSendEids() throws Exception {
+ byte[] eidBytes1 = {
+ (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+ (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+ (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+ (byte) 0xe1, (byte) 0xde
+ };
+ byte[] eidBytes2 = {
+ (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+ (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+ (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+ (byte) 0xf2, (byte) 0xef
+ };
+ PoweredOffFindingEphemeralId ephemeralId1 = new PoweredOffFindingEphemeralId();
+ PoweredOffFindingEphemeralId ephemeralId2 = new PoweredOffFindingEphemeralId();
+ ephemeralId1.bytes = eidBytes1;
+ ephemeralId2.bytes = eidBytes2;
+
+ mBluetoothFinderManager.sendEids(List.of(ephemeralId1, ephemeralId2));
+
+ verify(mIBluetoothFinderMock).sendEids(mEidArrayCaptor.capture());
+ assertThat(mEidArrayCaptor.getValue()[0].bytes).isEqualTo(eidBytes1);
+ assertThat(mEidArrayCaptor.getValue()[1].bytes).isEqualTo(eidBytes2);
+ }
+
+ @Test
+ public void testSendEids_remoteException() throws Exception {
+ doThrow(new RemoteException())
+ .when(mIBluetoothFinderMock).sendEids(any());
+ mBluetoothFinderManager.sendEids(List.of());
+
+ // Verify that we get the service again following a RemoteException.
+ mGetServiceCalled = false;
+ mBluetoothFinderManager.sendEids(List.of());
+ assertThat(mGetServiceCalled).isTrue();
+ }
+
+ @Test
+ public void testSendEids_serviceSpecificException() throws Exception {
+ doThrow(new ServiceSpecificException(1))
+ .when(mIBluetoothFinderMock).sendEids(any());
+ mBluetoothFinderManager.sendEids(List.of());
+ }
+
+ @Test
+ public void testSetPoweredOffFinderMode() throws Exception {
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+ verify(mIBluetoothFinderMock).setPoweredOffFinderMode(true);
+
+ mBluetoothFinderManager.setPoweredOffFinderMode(false);
+ verify(mIBluetoothFinderMock).setPoweredOffFinderMode(false);
+ }
+
+ @Test
+ public void testSetPoweredOffFinderMode_remoteException() throws Exception {
+ doThrow(new RemoteException())
+ .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+
+ // Verify that we get the service again following a RemoteException.
+ mGetServiceCalled = false;
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+ assertThat(mGetServiceCalled).isTrue();
+ }
+
+ @Test
+ public void testSetPoweredOffFinderMode_serviceSpecificException() throws Exception {
+ doThrow(new ServiceSpecificException(1))
+ .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+ }
+
+ @Test
+ public void testGetPoweredOffFinderMode() throws Exception {
+ when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenReturn(true);
+ assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isTrue();
+
+ when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenReturn(false);
+ assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+ }
+
+ @Test
+ public void testGetPoweredOffFinderMode_remoteException() throws Exception {
+ when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenThrow(new RemoteException());
+ assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+
+ // Verify that we get the service again following a RemoteException.
+ mGetServiceCalled = false;
+ assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+ assertThat(mGetServiceCalled).isTrue();
+ }
+
+ @Test
+ public void testGetPoweredOffFinderMode_serviceSpecificException() throws Exception {
+ when(mIBluetoothFinderMock.getPoweredOffFinderMode())
+ .thenThrow(new ServiceSpecificException(1));
+ assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+ }
+
+ @Test
+ public void testDeathRecipient() throws Exception {
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+ verify(mServiceBinderMock).linkToDeath(mDeathRecipientCaptor.capture(), anyInt());
+ mDeathRecipientCaptor.getValue().binderDied();
+
+ // Verify that we get the service again following a binder death.
+ mGetServiceCalled = false;
+ mBluetoothFinderManager.setPoweredOffFinderMode(true);
+ assertThat(mGetServiceCalled).isTrue();
+ }
+}
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
index 1f92374..c39b46c 100644
--- a/netbpfload/Android.bp
+++ b/netbpfload/Android.bp
@@ -14,6 +14,22 @@
// limitations under the License.
//
+package {
+ default_team: "trendy_team_fwk_core_networking",
+}
+
+install_symlink {
+ name: "mainline_tethering_platform_components",
+
+ symlink_target: "/apex/com.android.tethering/bin/ethtool",
+ // installed_location is relative to /system because that's the default partition for soong
+ // modules, unless we add something like `system_ext_specific: true` like in hwservicemanager.
+ installed_location: "bin/ethtool",
+
+ init_rc: ["netbpfload.rc"],
+ required: ["bpfloader"],
+}
+
cc_binary {
name: "netbpfload",
@@ -40,12 +56,22 @@
"com.android.tethering",
"//apex_available:platform",
],
- // really should be Android 14/U (34), but we cannot include binaries built
+ // really should be Android 13/T (33), but we cannot include binaries built
// against newer sdk in the apex, which still targets 30(R):
// module "netbpfload" variant "android_x86_apex30": should support
// min_sdk_version(30) for "com.android.tethering": newer SDK(34).
min_sdk_version: "30",
+ installable: false,
+}
- init_rc: ["netbpfload.rc"],
- required: ["bpfloader"],
+// Versioned netbpfload init rc: init system will process it only on api T/33+ devices
+// Note: R[30] S[31] Sv2[32] T[33] U[34] V[35])
+//
+// For details of versioned rc files see:
+// https://android.googlesource.com/platform/system/core/+/HEAD/init/README.md#versioned-rc-files-within-apexs
+prebuilt_etc {
+ name: "netbpfload.mainline.rc",
+ src: "netbpfload.mainline.rc",
+ filename: "netbpfload.33rc",
+ installable: false,
}
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index 6152287..83bb98c 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -97,7 +97,7 @@
},
};
-int loadAllElfObjects(const android::bpf::Location& location) {
+int loadAllElfObjects(const unsigned int bpfloader_ver, const android::bpf::Location& location) {
int retVal = 0;
DIR* dir;
struct dirent* ent;
@@ -111,7 +111,7 @@
progPath += s;
bool critical;
- int ret = android::bpf::loadProg(progPath.c_str(), &critical, location);
+ int ret = android::bpf::loadProg(progPath.c_str(), &critical, bpfloader_ver, location);
if (ret) {
if (critical) retVal = ret;
ALOGE("Failed to load object: %s, ret: %s", progPath.c_str(), std::strerror(-ret));
@@ -169,15 +169,116 @@
return 0;
}
+#define APEX_MOUNT_POINT "/apex/com.android.tethering"
+const char * const platformBpfLoader = "/system/bin/bpfloader";
+
+int logTetheringApexVersion(void) {
+ char * found_blockdev = NULL;
+ FILE * f = NULL;
+ char buf[4096];
+
+ f = fopen("/proc/mounts", "re");
+ if (!f) return 1;
+
+ // /proc/mounts format: block_device [space] mount_point [space] other stuff... newline
+ while (fgets(buf, sizeof(buf), f)) {
+ char * blockdev = buf;
+ char * space = strchr(blockdev, ' ');
+ if (!space) continue;
+ *space = '\0';
+ char * mntpath = space + 1;
+ space = strchr(mntpath, ' ');
+ if (!space) continue;
+ *space = '\0';
+ if (strcmp(mntpath, APEX_MOUNT_POINT)) continue;
+ found_blockdev = strdup(blockdev);
+ break;
+ }
+ fclose(f);
+ f = NULL;
+
+ if (!found_blockdev) return 2;
+ ALOGD("Found Tethering Apex mounted from blockdev %s", found_blockdev);
+
+ f = fopen("/proc/mounts", "re");
+ if (!f) { free(found_blockdev); return 3; }
+
+ while (fgets(buf, sizeof(buf), f)) {
+ char * blockdev = buf;
+ char * space = strchr(blockdev, ' ');
+ if (!space) continue;
+ *space = '\0';
+ char * mntpath = space + 1;
+ space = strchr(mntpath, ' ');
+ if (!space) continue;
+ *space = '\0';
+ if (strcmp(blockdev, found_blockdev)) continue;
+ if (strncmp(mntpath, APEX_MOUNT_POINT "@", strlen(APEX_MOUNT_POINT "@"))) continue;
+ char * at = strchr(mntpath, '@');
+ if (!at) continue;
+ char * ver = at + 1;
+ ALOGI("Tethering APEX version %s", ver);
+ }
+ fclose(f);
+ free(found_blockdev);
+ return 0;
+}
+
int main(int argc, char** argv, char * const envp[]) {
(void)argc;
android::base::InitLogging(argv, &android::base::KernelLogger);
- const int device_api_level = android_get_device_api_level();
- const bool isAtLeastU = (device_api_level >= __ANDROID_API_U__);
+ ALOGI("NetBpfLoad '%s' starting...", argv[0]);
- if (!android::bpf::isAtLeastKernelVersion(4, 19, 0)) {
- ALOGE("Android U QPR2 requires kernel 4.19.");
+ const int device_api_level = android_get_device_api_level();
+ const bool isAtLeastT = (device_api_level >= __ANDROID_API_T__);
+ const bool isAtLeastU = (device_api_level >= __ANDROID_API_U__);
+ const bool isAtLeastV = (device_api_level >= __ANDROID_API_V__);
+
+ // last in U QPR2 beta1
+ const bool has_platform_bpfloader_rc = exists("/system/etc/init/bpfloader.rc");
+ // first in U QPR2 beta~2
+ const bool has_platform_netbpfload_rc = exists("/system/etc/init/netbpfload.rc");
+
+ ALOGI("NetBpfLoad api:%d/%d kver:%07x rc:%d%d",
+ android_get_application_target_sdk_version(), device_api_level,
+ android::bpf::kernelVersion(),
+ has_platform_bpfloader_rc, has_platform_netbpfload_rc);
+
+ if (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
+ ALOGE("Unable to find platform's bpfloader & netbpfload init scripts.");
+ return 1;
+ }
+
+ if (has_platform_bpfloader_rc && has_platform_netbpfload_rc) {
+ ALOGE("Platform has *both* bpfloader & netbpfload init scripts.");
+ return 1;
+ }
+
+ logTetheringApexVersion();
+
+ if (!isAtLeastT) {
+ ALOGE("Impossible - not reachable on Android <T.");
+ return 1;
+ }
+
+ if (isAtLeastT && !android::bpf::isAtLeastKernelVersion(4, 9, 0)) {
+ ALOGE("Android T requires kernel 4.9.");
+ return 1;
+ }
+
+ if (isAtLeastU && !android::bpf::isAtLeastKernelVersion(4, 14, 0)) {
+ ALOGE("Android U requires kernel 4.14.");
+ return 1;
+ }
+
+ if (isAtLeastV && !android::bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ ALOGE("Android V requires kernel 4.19.");
+ return 1;
+ }
+
+ if (isAtLeastV && android::bpf::isX86() && !android::bpf::isKernel64Bit()) {
+ ALOGE("Android V requires X86 kernel to be 64-bit.");
return 1;
}
@@ -212,14 +313,16 @@
return 1;
}
- if (isAtLeastU) {
+ if (false && isAtLeastV) {
// Linux 5.16-rc1 changed the default to 2 (disabled but changeable),
// but we need 0 (enabled)
// (this writeFile is known to fail on at least 4.19, but always defaults to 0 on
// pre-5.13, on 5.13+ it depends on CONFIG_BPF_UNPRIV_DEFAULT_OFF)
if (writeProcSysFile("/proc/sys/kernel/unprivileged_bpf_disabled", "0\n") &&
android::bpf::isAtLeastKernelVersion(5, 13, 0)) return 1;
+ }
+ if (isAtLeastU) {
// Enable the eBPF JIT -- but do note that on 64-bit kernels it is likely
// already force enabled by the kernel config option BPF_JIT_ALWAYS_ON.
// (Note: this (open) will fail with ENOENT 'No such file or directory' if
@@ -242,9 +345,22 @@
if (createSysFsBpfSubDir(location.prefix)) return 1;
}
+ // Note: there's no actual src dir for fs_bpf_loader .o's,
+ // so it is not listed in 'locations[].prefix'.
+ // This is because this is primarily meant for triggering genfscon rules,
+ // and as such this will likely always be the case.
+ // Thus we need to manually create the /sys/fs/bpf/loader subdirectory.
+ if (createSysFsBpfSubDir("loader")) return 1;
+
+ // Version of Network BpfLoader depends on the Android OS version
+ unsigned int bpfloader_ver = 42u; // [42] BPFLOADER_MAINLINE_VERSION
+ if (isAtLeastT) ++bpfloader_ver; // [43] BPFLOADER_MAINLINE_T_VERSION
+ if (isAtLeastU) ++bpfloader_ver; // [44] BPFLOADER_MAINLINE_U_VERSION
+ if (isAtLeastV) ++bpfloader_ver; // [45] BPFLOADER_MAINLINE_V_VERSION
+
// Load all ELF objects, create programs and maps, and pin them
for (const auto& location : locations) {
- if (loadAllElfObjects(location) != 0) {
+ if (loadAllElfObjects(bpfloader_ver, location) != 0) {
ALOGE("=== CRITICAL FAILURE LOADING BPF PROGRAMS FROM %s ===", location.dir);
ALOGE("If this triggers reliably, you're probably missing kernel options or patches.");
ALOGE("If this triggers randomly, you might be hitting some memory allocation "
@@ -264,12 +380,15 @@
return 1;
}
- ALOGI("done, transferring control to platform bpfloader.");
+ if (false && isAtLeastV) {
+ ALOGI("done, transferring control to platform bpfloader.");
- const char * args[] = { "/system/bin/bpfloader", NULL, };
- if (execve(args[0], (char**)args, envp)) {
- ALOGE("FATAL: execve('/system/bin/bpfloader'): %d[%s]", errno, strerror(errno));
+ const char * args[] = { platformBpfLoader, NULL, };
+ execve(args[0], (char**)args, envp);
+ ALOGE("FATAL: execve('%s'): %d[%s]", platformBpfLoader, errno, strerror(errno));
+ return 1;
}
- return 1;
+ ALOGI("mainline done!");
+ return 0;
}
diff --git a/netbpfload/loader.cpp b/netbpfload/loader.cpp
index c534b2c..9dd0d2a 100644
--- a/netbpfload/loader.cpp
+++ b/netbpfload/loader.cpp
@@ -31,24 +31,11 @@
#include <sys/wait.h>
#include <unistd.h>
-// This is BpfLoader v0.41
-// WARNING: If you ever hit cherrypick conflicts here you're doing it wrong:
-// You are NOT allowed to cherrypick bpfloader related patches out of order.
-// (indeed: cherrypicking is probably a bad idea and you should merge instead)
-// Mainline supports ONLY the published versions of the bpfloader for each Android release.
-#define BPFLOADER_VERSION_MAJOR 0u
-#define BPFLOADER_VERSION_MINOR 41u
-#define BPFLOADER_VERSION ((BPFLOADER_VERSION_MAJOR << 16) | BPFLOADER_VERSION_MINOR)
-
#include "BpfSyscallWrappers.h"
#include "bpf/BpfUtils.h"
#include "bpf/bpf_map_def.h"
#include "loader.h"
-#if BPFLOADER_VERSION < COMPILE_FOR_BPFLOADER_VERSION
-#error "BPFLOADER_VERSION is less than COMPILE_FOR_BPFLOADER_VERSION"
-#endif
-
#include <cstdlib>
#include <fstream>
#include <iostream>
@@ -413,9 +400,6 @@
size_t sizeOfBpfProgDef) {
vector<char> pdData;
int ret = readSectionByName("progs", elfFile, pdData);
- // Older file formats do not require a 'progs' section at all.
- // (We should probably figure out whether this is behaviour which is safe to remove now.)
- if (ret == -2) return 0;
if (ret) return ret;
if (pdData.size() % sizeOfBpfProgDef) {
@@ -574,6 +558,14 @@
static bool mapMatchesExpectations(const unique_fd& fd, const string& mapName,
const struct bpf_map_def& mapDef, const enum bpf_map_type type) {
+ // bpfGetFd... family of functions require at minimum a 4.14 kernel,
+ // so on 4.9-T kernels just pretend the map matches our expectations.
+ // Additionally we'll get almost equivalent test coverage on newer devices/kernels.
+ // This is because the primary failure mode we're trying to detect here
+ // is either a source code misconfiguration (which is likely kernel independent)
+ // or a newly introduced kernel feature/bug (which is unlikely to get backported to 4.9).
+ if (!isAtLeastKernelVersion(4, 14, 0)) return true;
+
// Assuming fd is a valid Bpf Map file descriptor then
// all the following should always succeed on a 4.14+ kernel.
// If they somehow do fail, they'll return -1 (and set errno),
@@ -621,7 +613,8 @@
}
static int createMaps(const char* elfPath, ifstream& elfFile, vector<unique_fd>& mapFds,
- const char* prefix, const size_t sizeOfBpfMapDef) {
+ const char* prefix, const size_t sizeOfBpfMapDef,
+ const unsigned int bpfloader_ver) {
int ret;
vector<char> mdData;
vector<struct bpf_map_def> md;
@@ -663,14 +656,14 @@
for (int i = 0; i < (int)mapNames.size(); i++) {
if (md[i].zero != 0) abort();
- if (BPFLOADER_VERSION < md[i].bpfloader_min_ver) {
+ if (bpfloader_ver < md[i].bpfloader_min_ver) {
ALOGI("skipping map %s which requires bpfloader min ver 0x%05x", mapNames[i].c_str(),
md[i].bpfloader_min_ver);
mapFds.push_back(unique_fd());
continue;
}
- if (BPFLOADER_VERSION >= md[i].bpfloader_max_ver) {
+ if (bpfloader_ver >= md[i].bpfloader_max_ver) {
ALOGI("skipping map %s which requires bpfloader max ver 0x%05x", mapNames[i].c_str(),
md[i].bpfloader_max_ver);
mapFds.push_back(unique_fd());
@@ -711,6 +704,16 @@
}
enum bpf_map_type type = md[i].type;
+ if (type == BPF_MAP_TYPE_DEVMAP && !isAtLeastKernelVersion(4, 14, 0)) {
+ // On Linux Kernels older than 4.14 this map type doesn't exist, but it can kind
+ // of be approximated: ARRAY has the same userspace api, though it is not usable
+ // by the same ebpf programs. However, that's okay because the bpf_redirect_map()
+ // helper doesn't exist on 4.9-T anyway (so the bpf program would fail to load,
+ // and thus needs to be tagged as 4.14+ either way), so there's nothing useful you
+ // could do with a DEVMAP anyway (that isn't already provided by an ARRAY)...
+ // Hence using an ARRAY instead of a DEVMAP simply makes life easier for userspace.
+ type = BPF_MAP_TYPE_ARRAY;
+ }
if (type == BPF_MAP_TYPE_DEVMAP_HASH && !isAtLeastKernelVersion(5, 4, 0)) {
// On Linux Kernels older than 5.4 this map type doesn't exist, but it can kind
// of be approximated: HASH has the same userspace visible api.
@@ -766,7 +769,8 @@
.max_entries = max_entries,
.map_flags = md[i].map_flags,
};
- strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
+ if (isAtLeastKernelVersion(4, 14, 0))
+ strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
fd.reset(bpf(BPF_MAP_CREATE, req));
saved_errno = errno;
ALOGD("bpf_create_map name %s, ret: %d", mapNames[i].c_str(), fd.get());
@@ -910,7 +914,7 @@
}
static int loadCodeSections(const char* elfPath, vector<codeSection>& cs, const string& license,
- const char* prefix) {
+ const char* prefix, const unsigned int bpfloader_ver) {
unsigned kvers = kernelVersion();
if (!kvers) {
@@ -946,8 +950,8 @@
ALOGD("cs[%d].name:%s requires bpfloader version [0x%05x,0x%05x)", i, name.c_str(),
bpfMinVer, bpfMaxVer);
- if (BPFLOADER_VERSION < bpfMinVer) continue;
- if (BPFLOADER_VERSION >= bpfMaxVer) continue;
+ if (bpfloader_ver < bpfMinVer) continue;
+ if (bpfloader_ver >= bpfMaxVer) continue;
if ((cs[i].prog_def->ignore_on_eng && isEng()) ||
(cs[i].prog_def->ignore_on_user && isUser()) ||
@@ -1008,7 +1012,8 @@
.log_size = static_cast<__u32>(log_buf.size()),
.expected_attach_type = cs[i].expected_attach_type,
};
- strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
+ if (isAtLeastKernelVersion(4, 14, 0))
+ strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
fd.reset(bpf(BPF_PROG_LOAD, req));
ALOGD("BPF_PROG_LOAD call for %s (%s) returned fd: %d (%s)", elfPath,
@@ -1082,7 +1087,8 @@
return 0;
}
-int loadProg(const char* elfPath, bool* isCritical, const Location& location) {
+int loadProg(const char* const elfPath, bool* const isCritical, const unsigned int bpfloader_ver,
+ const Location& location) {
vector<char> license;
vector<char> critical;
vector<codeSection> cs;
@@ -1121,27 +1127,27 @@
readSectionUint("size_of_bpf_prog_def", elfFile, DEFAULT_SIZEOF_BPF_PROG_DEF);
// inclusive lower bound check
- if (BPFLOADER_VERSION < bpfLoaderMinVer) {
+ if (bpfloader_ver < bpfLoaderMinVer) {
ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with min ver 0x%05x",
- BPFLOADER_VERSION, elfPath, bpfLoaderMinVer);
+ bpfloader_ver, elfPath, bpfLoaderMinVer);
return 0;
}
// exclusive upper bound check
- if (BPFLOADER_VERSION >= bpfLoaderMaxVer) {
+ if (bpfloader_ver >= bpfLoaderMaxVer) {
ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with max ver 0x%05x",
- BPFLOADER_VERSION, elfPath, bpfLoaderMaxVer);
+ bpfloader_ver, elfPath, bpfLoaderMaxVer);
return 0;
}
- if (BPFLOADER_VERSION < bpfLoaderMinRequiredVer) {
+ if (bpfloader_ver < bpfLoaderMinRequiredVer) {
ALOGI("BpfLoader version 0x%05x failing due to ELF object %s with required min ver 0x%05x",
- BPFLOADER_VERSION, elfPath, bpfLoaderMinRequiredVer);
+ bpfloader_ver, elfPath, bpfLoaderMinRequiredVer);
return -1;
}
ALOGI("BpfLoader version 0x%05x processing ELF object %s with ver [0x%05x,0x%05x)",
- BPFLOADER_VERSION, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
+ bpfloader_ver, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
if (sizeOfBpfMapDef < DEFAULT_SIZEOF_BPF_MAP_DEF) {
ALOGE("sizeof(bpf_map_def) of %zu is too small (< %d)", sizeOfBpfMapDef,
@@ -1164,7 +1170,7 @@
/* Just for future debugging */
if (0) dumpAllCs(cs);
- ret = createMaps(elfPath, elfFile, mapFds, location.prefix, sizeOfBpfMapDef);
+ ret = createMaps(elfPath, elfFile, mapFds, location.prefix, sizeOfBpfMapDef, bpfloader_ver);
if (ret) {
ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
return ret;
@@ -1175,7 +1181,7 @@
applyMapRelo(elfFile, mapFds, cs);
- ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix);
+ ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix, bpfloader_ver);
if (ret) ALOGE("Failed to load programs, loadCodeSections ret=%d", ret);
return ret;
diff --git a/netbpfload/loader.h b/netbpfload/loader.h
index b884637..4da6830 100644
--- a/netbpfload/loader.h
+++ b/netbpfload/loader.h
@@ -70,7 +70,8 @@
};
// BPF loader implementation. Loads an eBPF ELF object
-int loadProg(const char* elfPath, bool* isCritical, const Location &location = {});
+int loadProg(const char* elfPath, bool* isCritical, const unsigned int bpfloader_ver,
+ const Location &location = {});
// Exposed for testing
unsigned int readSectionUint(const char* name, std::ifstream& elfFile, unsigned int defVal);
diff --git a/netbpfload/netbpfload.mainline.rc b/netbpfload/netbpfload.mainline.rc
new file mode 100644
index 0000000..d38a503
--- /dev/null
+++ b/netbpfload/netbpfload.mainline.rc
@@ -0,0 +1,17 @@
+service mdnsd_loadbpf /system/bin/bpfloader
+ capabilities CHOWN SYS_ADMIN NET_ADMIN
+ group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+ user root
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ reboot_on_failure reboot,bpfloader-failed
+
+service bpfloader /apex/com.android.tethering/bin/netbpfload
+ capabilities CHOWN SYS_ADMIN NET_ADMIN
+ group system root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw
+ user system
+ file /dev/kmsg w
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ reboot_on_failure reboot,bpfloader-failed
+ override
diff --git a/netbpfload/netbpfload.rc b/netbpfload/netbpfload.rc
index 14181dc..e1af47f 100644
--- a/netbpfload/netbpfload.rc
+++ b/netbpfload/netbpfload.rc
@@ -17,15 +17,18 @@
on load_bpf_programs
exec_start bpfloader
-service bpfloader /system/bin/netbpfload
+# Note: This will actually execute /apex/com.android.tethering/bin/netbpfload
+# by virtue of 'service bpfloader' being overridden by the apex shipped .rc
+# Warning: most of the below settings are irrelevant unless the apex is missing.
+service bpfloader /system/bin/false
# netbpfload will do network bpf loading, then execute /system/bin/bpfloader
- capabilities CHOWN SYS_ADMIN NET_ADMIN
+ #! capabilities CHOWN SYS_ADMIN NET_ADMIN
# The following group memberships are a workaround for lack of DAC_OVERRIDE
# and allow us to open (among other things) files that we created and are
# no longer root owned (due to CHOWN) but still have group read access to
# one of the following groups. This is not perfect, but a more correct
# solution requires significantly more effort to implement.
- group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+ #! group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
user root
#
# Set RLIMIT_MEMLOCK to 1GiB for bpfloader
@@ -55,7 +58,7 @@
#
# As such we simply use 1GiB as a reasonable approximation of infinity.
#
- rlimit memlock 1073741824 1073741824
+ #! rlimit memlock 1073741824 1073741824
oneshot
#
# How to debug bootloops caused by 'bpfloader-failed'.
@@ -81,6 +84,5 @@
# 'cannot prove return value is 0 or 1' or 'unsupported / unknown operation / helper',
# 'invalid bpf_context access', etc.
#
- reboot_on_failure reboot,bpfloader-failed
- # we're not really updatable, but want to be able to load bpf programs shipped in apexes
+ reboot_on_failure reboot,netbpfload-missing
updatable
diff --git a/netd/Android.bp b/netd/Android.bp
index 3cdbc97..eedbdae 100644
--- a/netd/Android.bp
+++ b/netd/Android.bp
@@ -14,6 +14,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -58,15 +59,18 @@
cc_test {
name: "netd_updatable_unit_test",
defaults: ["netd_defaults"],
- test_suites: ["general-tests", "mts-tethering"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
test_config_template: ":net_native_test_config_template",
- require_root: true, // required by setrlimitForTest()
+ require_root: true, // required by setrlimitForTest()
header_libs: [
"bpf_connectivity_headers",
],
srcs: [
"BpfHandlerTest.cpp",
- "BpfBaseTest.cpp"
+ "BpfBaseTest.cpp",
],
static_libs: [
"libbase",
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index a00c363..0d75c05 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -165,9 +165,38 @@
BpfHandler::BpfHandler(uint32_t perUidLimit, uint32_t totalLimit)
: mPerUidStatsEntriesLimit(perUidLimit), mTotalUidStatsEntriesLimit(totalLimit) {}
+// copied with minor changes from waitForProgsLoaded()
+// p/m/C's staticlibs/native/bpf_headers/include/bpf/WaitForProgsLoaded.h
+static inline void waitForNetProgsLoaded() {
+ // infinite loop until success with 5/10/20/40/60/60/60... delay
+ for (int delay = 5;; delay *= 2) {
+ if (delay > 60) delay = 60;
+ if (base::WaitForProperty("init.svc.bpfloader", "stopped", std::chrono::seconds(delay))
+ && !access("/sys/fs/bpf/netd_shared", F_OK))
+ return;
+ ALOGW("Waited %ds for init.svc.bpfloader=stopped, still waiting...", delay);
+ }
+}
+
Status BpfHandler::init(const char* cg2_path) {
- // Make sure BPF programs are loaded before doing anything
- android::bpf::waitForProgsLoaded();
+ if (base::GetProperty("bpf.progs_loaded", "") != "1") {
+ // Make sure BPF programs are loaded before doing anything
+ ALOGI("Waiting for BPF programs");
+
+ if (true || !modules::sdklevel::IsAtLeastV()) {
+ waitForNetProgsLoaded();
+ ALOGI("Networking BPF programs are loaded");
+
+ if (!base::SetProperty("ctl.start", "mdnsd_loadbpf")) {
+ ALOGE("Failed to set property ctl.start=mdnsd_loadbpf, see dmesg for reason.");
+ abort();
+ }
+
+ ALOGI("Waiting for remaining BPF programs");
+ }
+
+ android::bpf::waitForProgsLoaded();
+ }
ALOGI("BPF programs are loaded");
RETURN_IF_NOT_OK(initPrograms(cg2_path));
diff --git a/remoteauth/framework/Android.bp b/remoteauth/framework/Android.bp
index 71b621a..2f1737f 100644
--- a/remoteauth/framework/Android.bp
+++ b/remoteauth/framework/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/remoteauth/service/Android.bp b/remoteauth/service/Android.bp
index 8330efc..32ae54f 100644
--- a/remoteauth/service/Android.bp
+++ b/remoteauth/service/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
index a95a8fb..c0ac779 100644
--- a/remoteauth/service/jni/Android.bp
+++ b/remoteauth/service/jni/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
index 0a189f2..421fe7e 100644
--- a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
@@ -67,7 +67,7 @@
logger::init(
logger::Config::default()
.with_tag_on_device("remoteauth")
- .with_min_level(log::Level::Trace)
+ .with_max_level(log::LevelFilter::Trace)
.with_filter("trace,jni=info"),
);
}
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs b/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs
index ac2eb8c..e44ab8b 100644
--- a/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_protocol.rs
@@ -30,7 +30,7 @@
logger::init(
logger::Config::default()
.with_tag_on_device("remoteauth")
- .with_min_level(log::Level::Trace)
+ .with_max_level(log::LevelFilter::Trace)
.with_filter("trace,jni=info"),
);
get_boolean_result(native_init(env), "native_init")
diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
index 77e6f19..47b9e31 100644
--- a/remoteauth/tests/unit/Android.bp
+++ b/remoteauth/tests/unit/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index bd2f916..012c076 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -74,6 +75,7 @@
"com.android.tethering",
],
visibility: [
+ "//frameworks/base/services/tests/VpnTests",
"//frameworks/base/tests/vcn",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/tests:__subpackages__",
@@ -98,7 +100,7 @@
min_sdk_version: "21",
lint: {
error_checks: ["NewApi"],
- baseline_filename: "lint-baseline.xml",
+
},
srcs: [
"src/com/android/server/connectivity/mdns/**/*.java",
diff --git a/service-t/jni/com_android_server_net_NetworkStatsService.cpp b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
index 81912ae..c999398 100644
--- a/service-t/jni/com_android_server_net_NetworkStatsService.cpp
+++ b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
@@ -34,10 +34,17 @@
using android::bpf::bpfGetUidStats;
using android::bpf::bpfGetIfaceStats;
+using android::bpf::bpfRegisterIface;
using android::bpf::NetworkTraceHandler;
namespace android {
+static void nativeRegisterIface(JNIEnv* env, jclass clazz, jstring iface) {
+ ScopedUtfChars iface8(env, iface);
+ if (iface8.c_str() == nullptr) return;
+ bpfRegisterIface(iface8.c_str());
+}
+
static jobject statsValueToEntry(JNIEnv* env, StatsValue* stats) {
// Find the Java class that represents the structure
jclass gEntryClass = env->FindClass("android/net/NetworkStats$Entry");
@@ -45,8 +52,14 @@
return nullptr;
}
+ // Find the constructor.
+ jmethodID constructorID = env->GetMethodID(gEntryClass, "<init>", "()V");
+ if (constructorID == nullptr) {
+ return nullptr;
+ }
+
// Create a new instance of the Java class
- jobject result = env->AllocObject(gEntryClass);
+ jobject result = env->NewObject(gEntryClass, constructorID);
if (result == nullptr) {
return nullptr;
}
@@ -63,7 +76,7 @@
static jobject nativeGetTotalStat(JNIEnv* env, jclass clazz) {
StatsValue stats = {};
- if (bpfGetIfaceStats(NULL, &stats) == 0) {
+ if (bpfGetIfaceStats(nullptr, &stats) == 0) {
return statsValueToEntry(env, &stats);
} else {
return nullptr;
@@ -72,7 +85,7 @@
static jobject nativeGetIfaceStat(JNIEnv* env, jclass clazz, jstring iface) {
ScopedUtfChars iface8(env, iface);
- if (iface8.c_str() == NULL) {
+ if (iface8.c_str() == nullptr) {
return nullptr;
}
@@ -101,6 +114,11 @@
static const JNINativeMethod gMethods[] = {
{
+ "nativeRegisterIface",
+ "(Ljava/lang/String;)V",
+ (void*)nativeRegisterIface
+ },
+ {
"nativeGetTotalStat",
"()Landroid/net/NetworkStats$Entry;",
(void*)nativeGetTotalStat
diff --git a/service-t/lint-baseline.xml b/service-t/lint-baseline.xml
index 38d3ab0..e4b92d6 100644
--- a/service-t/lint-baseline.xml
+++ b/service-t/lint-baseline.xml
@@ -1,104 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier#getInterfaceName`"
- errorLine1=" if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
- line="224"
- column="48"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getInterface`"
- errorLine1=" delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
- line="276"
- column="55"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getOwnerUid`"
- errorLine1=" delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
- line="276"
- column="35"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getUnderlyingInterfaces`"
- errorLine1=" info.getUnderlyingInterfaces());"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
- line="277"
- column="26"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
- errorLine1=" dnsAddresses.add(InetAddress.parseNumericAddress(address));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
- line="875"
- column="54"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
- errorLine1=" staticIpConfigBuilder.setGateway(InetAddress.parseNumericAddress(value));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
- line="870"
- column="66"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(os);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsRecorder.java"
- line="556"
- column="25"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(sockFd);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
- line="1309"
- column="25"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(mSocket);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
- line="1034"
- column="21"/>
- </issue>
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
<issue
id="NewApi"
@@ -113,6 +14,17 @@
<issue
id="NewApi"
+ message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
+ errorLine1=" .setNetworkSpecifier(new EthernetNetworkSpecifier(ifaceName))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java"
+ line="156"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="NewApi"
message="Call requires API level 31 (current min is 30): `new android.net.EthernetNetworkSpecifier`"
errorLine1=" nc.setNetworkSpecifier(new EthernetNetworkSpecifier(iface));"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -124,39 +36,6 @@
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `new android.util.AtomicFile`"
- errorLine1=" mFile = new AtomicFile(new File(path), logger);"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/net/PersistentInt.java"
- line="53"
- column="17"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `new java.net.InetSocketAddress`"
- errorLine1=" super(handler, new RecvBuffer(buffer, new InetSocketAddress()));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java"
- line="66"
- column="47"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
- errorLine1=" .setNetworkSpecifier(new EthernetNetworkSpecifier(ifaceName))"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java"
- line="156"
- column="38"/>
- </issue>
-
- <issue
- id="NewApi"
message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
errorLine1=" nc.setNetworkSpecifier(new EthernetNetworkSpecifier(iface));"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -169,6 +48,28 @@
<issue
id="NewApi"
message="Class requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier`"
+ errorLine1=" if (!(spec instanceof EthernetNetworkSpecifier)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+ line="221"
+ column="31"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier#getInterfaceName`"
+ errorLine1=" if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+ line="224"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier`"
errorLine1=" if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@@ -179,13 +80,178 @@
<issue
id="NewApi"
- message="Class requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier`"
- errorLine1=" if (!(spec instanceof EthernetNetworkSpecifier)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+ errorLine1=" staticIpConfigBuilder.setGateway(InetAddress.parseNumericAddress(value));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
- file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
- line="221"
- column="31"/>
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
+ line="885"
+ column="66"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+ errorLine1=" dnsAddresses.add(InetAddress.parseNumericAddress(address));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
+ line="890"
+ column="54"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(mSocket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+ line="1042"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(sockFd);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+ line="1318"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 30): `android.system.OsConstants#UDP_ENCAP`"
+ errorLine1=" OsConstants.UDP_ENCAP,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+ line="1326"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 30): `android.system.OsConstants#UDP_ENCAP_ESPINUDP`"
+ errorLine1=" OsConstants.UDP_ENCAP_ESPINUDP);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+ line="1327"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `BpfNetMaps`"
+ errorLine1=" return new BpfNetMaps(ctx);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="111"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `swapActiveStatsMap`"
+ errorLine1=" mBpfNetMaps.swapActiveStatsMap();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="185"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getInterface`"
+ errorLine1=" delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="240"
+ column="55"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getOwnerUid`"
+ errorLine1=" delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="240"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getUnderlyingInterfaces`"
+ errorLine1=" info.getUnderlyingInterfaces());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="241"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(os);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsRecorder.java"
+ line="580"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 35 (current min is 34): `newInstance`"
+ errorLine1=" opts = BroadcastOptionsShimImpl.newInstance("
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsService.java"
+ line="562"
+ column="61"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.util.AtomicFile`"
+ errorLine1=" mFile = new AtomicFile(new File(path), logger);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/PersistentInt.java"
+ line="53"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `addOrUpdateInterfaceAddress`"
+ errorLine1=" mCb.addOrUpdateInterfaceAddress(ifaddrMsg.index, la);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java"
+ line="69"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `deleteInterfaceAddress`"
+ errorLine1=" mCb.deleteInterfaceAddress(ifaddrMsg.index, la);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java"
+ line="73"
+ column="21"/>
</issue>
</issues>
\ No newline at end of file
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
index b9f3adb..c620634 100644
--- a/service-t/native/libs/libnetworkstats/Android.bp
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -57,9 +58,12 @@
cc_test {
name: "libnetworkstats_test",
- test_suites: ["general-tests", "mts-tethering"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
test_config_template: ":net_native_test_config_template",
- require_root: true, // required by setrlimitForTest()
+ require_root: true, // required by setrlimitForTest()
header_libs: ["bpf_connectivity_headers"],
srcs: [
"BpfNetworkStatsTest.cpp",
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
index 3101397..d3e331e 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -40,6 +40,35 @@
using base::Result;
+BpfMap<uint32_t, IfaceValue>& getIfaceIndexNameMap() {
+ static BpfMap<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+ return ifaceIndexNameMap;
+}
+
+const BpfMapRO<uint32_t, StatsValue>& getIfaceStatsMap() {
+ static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
+ return ifaceStatsMap;
+}
+
+Result<IfaceValue> ifindex2name(const uint32_t ifindex) {
+ Result<IfaceValue> v = getIfaceIndexNameMap().readValue(ifindex);
+ if (v.ok()) return v;
+ IfaceValue iv = {};
+ if (!if_indextoname(ifindex, iv.name)) return v;
+ getIfaceIndexNameMap().writeValue(ifindex, iv, BPF_ANY);
+ return iv;
+}
+
+void bpfRegisterIface(const char* iface) {
+ if (!iface) return;
+ if (strlen(iface) >= sizeof(IfaceValue)) return;
+ uint32_t ifindex = if_nametoindex(iface);
+ if (!ifindex) return;
+ IfaceValue ifname = {};
+ strlcpy(ifname.name, iface, sizeof(ifname.name));
+ getIfaceIndexNameMap().writeValue(ifindex, ifname, BPF_ANY);
+}
+
int bpfGetUidStatsInternal(uid_t uid, StatsValue* stats,
const BpfMapRO<uint32_t, StatsValue>& appUidStatsMap) {
auto statsEntry = appUidStatsMap.readValue(uid);
@@ -58,19 +87,19 @@
int bpfGetIfaceStatsInternal(const char* iface, StatsValue* stats,
const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap,
- const BpfMapRO<uint32_t, IfaceValue>& ifaceNameMap) {
+ const IfIndexToNameFunc ifindex2name) {
*stats = {};
int64_t unknownIfaceBytesTotal = 0;
const auto processIfaceStats =
- [iface, stats, &ifaceNameMap, &unknownIfaceBytesTotal](
+ [iface, stats, ifindex2name, &unknownIfaceBytesTotal](
const uint32_t& key,
const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap) -> Result<void> {
- char ifname[IFNAMSIZ];
- if (getIfaceNameFromMap(ifaceNameMap, ifaceStatsMap, key, ifname, key,
- &unknownIfaceBytesTotal)) {
+ Result<IfaceValue> ifname = ifindex2name(key);
+ if (!ifname.ok()) {
+ maybeLogUnknownIface(key, ifaceStatsMap, key, &unknownIfaceBytesTotal);
return Result<void>();
}
- if (!iface || !strcmp(iface, ifname)) {
+ if (!iface || !strcmp(iface, ifname.value().name)) {
Result<StatsValue> statsEntry = ifaceStatsMap.readValue(key);
if (!statsEntry.ok()) {
return statsEntry.error();
@@ -84,9 +113,7 @@
}
int bpfGetIfaceStats(const char* iface, StatsValue* stats) {
- static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
- static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
- return bpfGetIfaceStatsInternal(iface, stats, ifaceStatsMap, ifaceIndexNameMap);
+ return bpfGetIfaceStatsInternal(iface, stats, getIfaceStatsMap(), ifindex2name);
}
int bpfGetIfIndexStatsInternal(uint32_t ifindex, StatsValue* stats,
@@ -101,14 +128,13 @@
}
int bpfGetIfIndexStats(int ifindex, StatsValue* stats) {
- static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
- return bpfGetIfIndexStatsInternal(ifindex, stats, ifaceStatsMap);
+ return bpfGetIfIndexStatsInternal(ifindex, stats, getIfaceStatsMap());
}
stats_line populateStatsEntry(const StatsKey& statsKey, const StatsValue& statsEntry,
- const char* ifname) {
+ const IfaceValue& ifname) {
stats_line newLine;
- strlcpy(newLine.iface, ifname, sizeof(newLine.iface));
+ strlcpy(newLine.iface, ifname.name, sizeof(newLine.iface));
newLine.uid = (int32_t)statsKey.uid;
newLine.set = (int32_t)statsKey.counterSet;
newLine.tag = (int32_t)statsKey.tag;
@@ -121,22 +147,22 @@
int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>& lines,
const BpfMapRO<StatsKey, StatsValue>& statsMap,
- const BpfMapRO<uint32_t, IfaceValue>& ifaceMap) {
+ const IfIndexToNameFunc ifindex2name) {
int64_t unknownIfaceBytesTotal = 0;
const auto processDetailUidStats =
- [&lines, &unknownIfaceBytesTotal, &ifaceMap](
+ [&lines, &unknownIfaceBytesTotal, &ifindex2name](
const StatsKey& key,
const BpfMapRO<StatsKey, StatsValue>& statsMap) -> Result<void> {
- char ifname[IFNAMSIZ];
- if (getIfaceNameFromMap(ifaceMap, statsMap, key.ifaceIndex, ifname, key,
- &unknownIfaceBytesTotal)) {
+ Result<IfaceValue> ifname = ifindex2name(key.ifaceIndex);
+ if (!ifname.ok()) {
+ maybeLogUnknownIface(key.ifaceIndex, statsMap, key, &unknownIfaceBytesTotal);
return Result<void>();
}
Result<StatsValue> statsEntry = statsMap.readValue(key);
if (!statsEntry.ok()) {
return base::ResultError(statsEntry.error().message(), statsEntry.error().code());
}
- stats_line newLine = populateStatsEntry(key, statsEntry.value(), ifname);
+ stats_line newLine = populateStatsEntry(key, statsEntry.value(), ifname.value());
lines.push_back(newLine);
if (newLine.tag) {
// account tagged traffic in the untagged stats (for historical reasons?)
@@ -166,7 +192,6 @@
}
int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines) {
- static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
static BpfMapRO<uint32_t, uint32_t> configurationMap(CONFIGURATION_MAP_PATH);
static BpfMap<StatsKey, StatsValue> statsMapA(STATS_MAP_A_PATH);
static BpfMap<StatsKey, StatsValue> statsMapB(STATS_MAP_B_PATH);
@@ -196,7 +221,7 @@
// TODO: the above comment feels like it may be obsolete / out of date,
// since we no longer swap the map via netd binder rpc - though we do
// still swap it.
- int ret = parseBpfNetworkStatsDetailInternal(*lines, *inactiveStatsMap, ifaceIndexNameMap);
+ int ret = parseBpfNetworkStatsDetailInternal(*lines, *inactiveStatsMap, ifindex2name);
if (ret) {
ALOGE("parse detail network stats failed: %s", strerror(errno));
return ret;
@@ -213,13 +238,14 @@
int parseBpfNetworkStatsDevInternal(std::vector<stats_line>& lines,
const BpfMapRO<uint32_t, StatsValue>& statsMap,
- const BpfMapRO<uint32_t, IfaceValue>& ifaceMap) {
+ const IfIndexToNameFunc ifindex2name) {
int64_t unknownIfaceBytesTotal = 0;
- const auto processDetailIfaceStats = [&lines, &unknownIfaceBytesTotal, &ifaceMap, &statsMap](
+ const auto processDetailIfaceStats = [&lines, &unknownIfaceBytesTotal, ifindex2name, &statsMap](
const uint32_t& key, const StatsValue& value,
const BpfMapRO<uint32_t, StatsValue>&) {
- char ifname[IFNAMSIZ];
- if (getIfaceNameFromMap(ifaceMap, statsMap, key, ifname, key, &unknownIfaceBytesTotal)) {
+ Result<IfaceValue> ifname = ifindex2name(key);
+ if (!ifname.ok()) {
+ maybeLogUnknownIface(key, statsMap, key, &unknownIfaceBytesTotal);
return Result<void>();
}
StatsKey fakeKey = {
@@ -227,7 +253,7 @@
.tag = (uint32_t)TAG_NONE,
.counterSet = (uint32_t)SET_ALL,
};
- lines.push_back(populateStatsEntry(fakeKey, value, ifname));
+ lines.push_back(populateStatsEntry(fakeKey, value, ifname.value()));
return Result<void>();
};
Result<void> res = statsMap.iterateWithValue(processDetailIfaceStats);
@@ -242,9 +268,7 @@
}
int parseBpfNetworkStatsDev(std::vector<stats_line>* lines) {
- static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
- static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
- return parseBpfNetworkStatsDevInternal(*lines, ifaceStatsMap, ifaceIndexNameMap);
+ return parseBpfNetworkStatsDevInternal(*lines, getIfaceStatsMap(), ifindex2name);
}
void groupNetworkStats(std::vector<stats_line>& lines) {
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
index bcc4550..484c166 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
@@ -77,6 +77,10 @@
BpfMap<uint32_t, IfaceValue> mFakeIfaceIndexNameMap;
BpfMap<uint32_t, StatsValue> mFakeIfaceStatsMap;
+ IfIndexToNameFunc mIfIndex2Name = [this](const uint32_t ifindex){
+ return mFakeIfaceIndexNameMap.readValue(ifindex);
+ };
+
void SetUp() {
ASSERT_EQ(0, setrlimitForTest());
@@ -228,7 +232,7 @@
populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET1, value1, mFakeStatsMap);
populateFakeStats(TEST_UID2, 0, IFACE_INDEX3, TEST_COUNTERSET1, value1, mFakeStatsMap);
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((unsigned long)3, lines.size());
}
@@ -256,16 +260,15 @@
EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
StatsValue result1 = {};
- ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME1, &result1, mFakeIfaceStatsMap,
- mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0,
+ bpfGetIfaceStatsInternal(IFACE_NAME1, &result1, mFakeIfaceStatsMap, mIfIndex2Name));
expectStatsEqual(value1, result1);
StatsValue result2 = {};
- ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME2, &result2, mFakeIfaceStatsMap,
- mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0,
+ bpfGetIfaceStatsInternal(IFACE_NAME2, &result2, mFakeIfaceStatsMap, mIfIndex2Name));
expectStatsEqual(value2, result2);
StatsValue totalResult = {};
- ASSERT_EQ(0, bpfGetIfaceStatsInternal(NULL, &totalResult, mFakeIfaceStatsMap,
- mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, bpfGetIfaceStatsInternal(NULL, &totalResult, mFakeIfaceStatsMap, mIfIndex2Name));
StatsValue totalValue = {
.rxPackets = TEST_PACKET0 * 2 + TEST_PACKET1,
.rxBytes = TEST_BYTES0 * 2 + TEST_BYTES1,
@@ -304,7 +307,7 @@
mFakeStatsMap);
populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
std::vector<stats_line> lines;
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((unsigned long)7, lines.size());
}
@@ -324,7 +327,7 @@
populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET1, value1, mFakeStatsMap);
populateFakeStats(TEST_UID2, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
std::vector<stats_line> lines;
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((unsigned long)4, lines.size());
}
@@ -352,18 +355,20 @@
.counterSet = TEST_COUNTERSET0,
.ifaceIndex = ifaceIndex,
};
- char ifname[IFNAMSIZ];
int64_t unknownIfaceBytesTotal = 0;
- ASSERT_EQ(-ENODEV, getIfaceNameFromMap(mFakeIfaceIndexNameMap, mFakeStatsMap, ifaceIndex,
- ifname, curKey, &unknownIfaceBytesTotal));
+ ASSERT_EQ(false, mFakeIfaceIndexNameMap.readValue(ifaceIndex).ok());
+ maybeLogUnknownIface(ifaceIndex, mFakeStatsMap, curKey, &unknownIfaceBytesTotal);
+
ASSERT_EQ(((int64_t)(TEST_BYTES0 * 20 + TEST_BYTES1 * 20)), unknownIfaceBytesTotal);
curKey.ifaceIndex = IFACE_INDEX2;
- ASSERT_EQ(-ENODEV, getIfaceNameFromMap(mFakeIfaceIndexNameMap, mFakeStatsMap, ifaceIndex,
- ifname, curKey, &unknownIfaceBytesTotal));
+
+ ASSERT_EQ(false, mFakeIfaceIndexNameMap.readValue(ifaceIndex).ok());
+ maybeLogUnknownIface(ifaceIndex, mFakeStatsMap, curKey, &unknownIfaceBytesTotal);
+
ASSERT_EQ(-1, unknownIfaceBytesTotal);
std::vector<stats_line> lines;
// TODO: find a way to test the total of unknown Iface Bytes go above limit.
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((unsigned long)1, lines.size());
expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines.front());
}
@@ -394,8 +399,7 @@
ifaceStatsKey = IFACE_INDEX4;
EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
std::vector<stats_line> lines;
- ASSERT_EQ(0,
- parseBpfNetworkStatsDevInternal(lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDevInternal(lines, mFakeIfaceStatsMap, mIfIndex2Name));
ASSERT_EQ((unsigned long)4, lines.size());
expectStatsLineEqual(value1, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
@@ -439,13 +443,13 @@
std::vector<stats_line> lines;
// Test empty stats.
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((size_t) 0, lines.size());
lines.clear();
// Test 1 line stats.
populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((size_t) 2, lines.size()); // TEST_TAG != 0 -> 1 entry becomes 2 lines
expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines[0]);
expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[1]);
@@ -457,7 +461,7 @@
populateFakeStats(TEST_UID1, TEST_TAG + 1, IFACE_INDEX1, TEST_COUNTERSET0, value2,
mFakeStatsMap);
populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((size_t) 9, lines.size());
lines.clear();
@@ -465,7 +469,7 @@
populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((size_t) 9, lines.size());
// Verify Sorted & Grouped.
@@ -490,8 +494,7 @@
ifaceStatsKey = IFACE_INDEX3;
EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
- ASSERT_EQ(0,
- parseBpfNetworkStatsDevInternal(lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDevInternal(lines, mFakeIfaceStatsMap, mIfIndex2Name));
ASSERT_EQ((size_t) 2, lines.size());
expectStatsLineEqual(value3, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
@@ -532,7 +535,7 @@
// TODO: Mutate counterSet and enlarge TEST_MAP_SIZE if overflow on counterSet is possible.
std::vector<stats_line> lines;
- ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mIfIndex2Name));
ASSERT_EQ((size_t) 12, lines.size());
// Uid 0 first
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
index 8058d05..59eb195 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
@@ -55,36 +55,27 @@
bool operator==(const stats_line& lhs, const stats_line& rhs);
bool operator<(const stats_line& lhs, const stats_line& rhs);
+// This mirrors BpfMap.h's:
+// Result<Value> readValue(const Key key) const
+// for a BpfMap<uint32_t, IfaceValue>
+using IfIndexToNameFunc = std::function<Result<IfaceValue>(const uint32_t)>;
+
// For test only
int bpfGetUidStatsInternal(uid_t uid, StatsValue* stats,
const BpfMapRO<uint32_t, StatsValue>& appUidStatsMap);
// For test only
int bpfGetIfaceStatsInternal(const char* iface, StatsValue* stats,
const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap,
- const BpfMapRO<uint32_t, IfaceValue>& ifaceNameMap);
+ const IfIndexToNameFunc ifindex2name);
// For test only
int bpfGetIfIndexStatsInternal(uint32_t ifindex, StatsValue* stats,
const BpfMapRO<uint32_t, StatsValue>& ifaceStatsMap);
// For test only
int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>& lines,
const BpfMapRO<StatsKey, StatsValue>& statsMap,
- const BpfMapRO<uint32_t, IfaceValue>& ifaceMap);
+ const IfIndexToNameFunc ifindex2name);
// For test only
int cleanStatsMapInternal(const base::unique_fd& cookieTagMap, const base::unique_fd& tagStatsMap);
-// For test only
-template <class Key>
-int getIfaceNameFromMap(const BpfMapRO<uint32_t, IfaceValue>& ifaceMap,
- const BpfMapRO<Key, StatsValue>& statsMap,
- uint32_t ifaceIndex, char* ifname,
- const Key& curKey, int64_t* unknownIfaceBytesTotal) {
- auto iface = ifaceMap.readValue(ifaceIndex);
- if (!iface.ok()) {
- maybeLogUnknownIface(ifaceIndex, statsMap, curKey, unknownIfaceBytesTotal);
- return -ENODEV;
- }
- strlcpy(ifname, iface.value().name, sizeof(IfaceValue));
- return 0;
-}
template <class Key>
void maybeLogUnknownIface(int ifaceIndex, const BpfMapRO<Key, StatsValue>& statsMap,
@@ -112,8 +103,9 @@
// For test only
int parseBpfNetworkStatsDevInternal(std::vector<stats_line>& lines,
const BpfMapRO<uint32_t, StatsValue>& statsMap,
- const BpfMapRO<uint32_t, IfaceValue>& ifaceMap);
+ const IfIndexToNameFunc ifindex2name);
+void bpfRegisterIface(const char* iface);
int bpfGetUidStats(uid_t uid, StatsValue* stats);
int bpfGetIfaceStats(const char* iface, StatsValue* stats);
int bpfGetIfIndexStats(int ifindex, StatsValue* stats);
diff --git a/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java b/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java
index 3ed21a2..08a8603 100644
--- a/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java
+++ b/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java
@@ -109,7 +109,7 @@
*/
private static Pair<Integer, Integer> getStatsFilesAttributes(
@Nullable File statsDir, @NonNull String prefix) {
- if (statsDir == null) return new Pair<>(0, 0);
+ if (statsDir == null || !statsDir.isDirectory()) return new Pair<>(0, 0);
// Only counts the matching files.
// The files are named in the following format:
@@ -118,9 +118,6 @@
// See FileRotator#FileInfo for more detail.
final Pattern pattern = Pattern.compile("^" + prefix + "\\.[0-9]+-[0-9]*$");
- // Ensure that base path exists.
- statsDir.mkdirs();
-
int totalFiles = 0;
int totalBytes = 0;
for (String name : emptyIfNull(statsDir.list())) {
diff --git a/service-t/src/com/android/server/IpSecXfrmController.java b/service-t/src/com/android/server/IpSecXfrmController.java
index c8abd40..3cfbf83 100644
--- a/service-t/src/com/android/server/IpSecXfrmController.java
+++ b/service-t/src/com/android/server/IpSecXfrmController.java
@@ -15,6 +15,7 @@
*/
package com.android.server;
+import static com.android.net.module.util.netlink.NetlinkUtils.SOCKET_RECV_BUFSIZE;
import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.NETLINK_XFRM;
import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_MSG_NEWSA;
@@ -106,7 +107,8 @@
public static class Dependencies {
/** Get a new XFRM netlink socket and connect it */
public FileDescriptor newNetlinkSocket() throws ErrnoException, SocketException {
- final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_XFRM);
+ final FileDescriptor fd =
+ NetlinkUtils.netlinkSocketForProto(NETLINK_XFRM, SOCKET_RECV_BUFSIZE);
NetlinkUtils.connectToKernel(fd);
return fd;
}
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 76481c8..8552eec 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -26,8 +26,9 @@
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.SUBTYPE_LABEL_REGEX;
import static android.net.nsd.NsdManager.TYPE_REGEX;
-import static android.net.nsd.NsdManager.TYPE_SUBTYPE_LABEL_REGEX;
+import static android.os.Process.SYSTEM_UID;
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
@@ -35,6 +36,8 @@
import static com.android.server.connectivity.mdns.MdnsAdvertiser.AdvertiserMetrics;
import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
import static com.android.server.connectivity.mdns.MdnsRecord.MAX_LABEL_LENGTH;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
import android.annotation.NonNull;
@@ -54,6 +57,7 @@
import android.net.mdns.aidl.RegistrationInfo;
import android.net.mdns.aidl.ResolutionInfo;
import android.net.nsd.AdvertisingRequest;
+import android.net.nsd.DiscoveryRequest;
import android.net.nsd.INsdManager;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.INsdServiceConnector;
@@ -89,6 +93,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.InetAddressUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
@@ -112,6 +117,7 @@
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -176,7 +182,7 @@
"mdns_advertiser_allowlist_";
private static final String MDNS_ALLOWLIST_FLAG_SUFFIX = "_version";
-
+ private static final String FORCE_ENABLE_FLAG_FOR_TEST_PREFIX = "test_";
@VisibleForTesting
static final String MDNS_CONFIG_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF =
@@ -250,6 +256,8 @@
private final RemoteCallbackList<IOffloadEngine> mOffloadEngines =
new RemoteCallbackList<>();
+ @NonNull
+ private final MdnsFeatureFlags mMdnsFeatureFlags;
private static class OffloadEngineInfo {
@NonNull final String mInterfaceName;
@@ -267,19 +275,15 @@
}
@VisibleForTesting
- static class MdnsListener implements MdnsServiceBrowserListener {
+ abstract static class MdnsListener implements MdnsServiceBrowserListener {
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;
}
@@ -317,13 +321,17 @@
@Override
public void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) { }
+
+ // Ensure toString gets overridden
+ @NonNull
+ public abstract String toString();
}
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
@@ -347,13 +355,21 @@
mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
DISCOVERY_QUERY_SENT_CALLBACK, new MdnsEvent(mClientRequestId));
}
+
+ @NonNull
+ @Override
+ public String toString() {
+ return String.format("DiscoveryListener: serviceType=%s", getListenedServiceType());
+ }
}
private class ResolutionListener extends MdnsListener {
+ private final String mServiceName;
ResolutionListener(int clientRequestId, int transactionId,
- @NonNull NsdServiceInfo reqServiceInfo, @NonNull String listenServiceType) {
- super(clientRequestId, transactionId, reqServiceInfo, listenServiceType);
+ @NonNull String listenServiceType, @NonNull String serviceName) {
+ super(clientRequestId, transactionId, listenServiceType);
+ mServiceName = serviceName;
}
@Override
@@ -369,13 +385,22 @@
mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
DISCOVERY_QUERY_SENT_CALLBACK, new MdnsEvent(mClientRequestId));
}
+
+ @NonNull
+ @Override
+ public String toString() {
+ return String.format("ResolutionListener serviceName=%s, serviceType=%s",
+ mServiceName, getListenedServiceType());
+ }
}
private class ServiceInfoListener extends MdnsListener {
+ private final String mServiceName;
ServiceInfoListener(int clientRequestId, int transactionId,
- @NonNull NsdServiceInfo reqServiceInfo, @NonNull String listenServiceType) {
- super(clientRequestId, transactionId, reqServiceInfo, listenServiceType);
+ @NonNull String listenServiceType, @NonNull String serviceName) {
+ super(clientRequestId, transactionId, listenServiceType);
+ this.mServiceName = serviceName;
}
@Override
@@ -406,6 +431,13 @@
mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
DISCOVERY_QUERY_SENT_CALLBACK, new MdnsEvent(mClientRequestId));
}
+
+ @NonNull
+ @Override
+ public String toString() {
+ return String.format("ServiceInfoListener serviceName=%s, serviceType=%s",
+ mServiceName, getListenedServiceType());
+ }
}
private class SocketRequestMonitor implements MdnsSocketProvider.SocketRequestMonitor {
@@ -542,13 +574,13 @@
}
private void maybeStartDaemon() {
- if (mMDnsManager == null) {
- Log.wtf(TAG, "maybeStartDaemon: mMDnsManager is null");
+ if (mIsDaemonStarted) {
+ if (DBG) Log.d(TAG, "Daemon is already started.");
return;
}
- if (mIsDaemonStarted) {
- if (DBG) Log.d(TAG, "Daemon is already started.");
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "maybeStartDaemon: mMDnsManager is null");
return;
}
mMDnsManager.registerEventListener(mMDnsEventCallback);
@@ -559,13 +591,13 @@
}
private void maybeStopDaemon() {
- if (mMDnsManager == null) {
- Log.wtf(TAG, "maybeStopDaemon: mMDnsManager is null");
+ if (!mIsDaemonStarted) {
+ if (DBG) Log.d(TAG, "Daemon has not been started.");
return;
}
- if (!mIsDaemonStarted) {
- if (DBG) Log.d(TAG, "Daemon has not been started.");
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "maybeStopDaemon: mMDnsManager is null");
return;
}
mMDnsManager.unregisterEventListener(mMDnsEventCallback);
@@ -652,9 +684,12 @@
}
private void storeAdvertiserRequestMap(int clientRequestId, int transactionId,
- ClientInfo clientInfo, @Nullable Network requestedNetwork) {
+ ClientInfo clientInfo, @NonNull NsdServiceInfo serviceInfo) {
+ final String serviceFullName =
+ serviceInfo.getServiceName() + "." + serviceInfo.getServiceType();
clientInfo.mClientRequests.put(clientRequestId, new AdvertiserClientRequest(
- transactionId, requestedNetwork, mClock.elapsedRealtime()));
+ transactionId, serviceInfo.getNetwork(), serviceFullName,
+ mClock.elapsedRealtime()));
mTransactionIdToClientInfoMap.put(transactionId, clientInfo);
updateMulticastLock();
}
@@ -737,6 +772,33 @@
return new ArraySet<>(subtypeMap.values());
}
+ private boolean checkTtl(
+ @Nullable Duration ttl, @NonNull ClientInfo clientInfo) {
+ if (ttl == null) {
+ return true;
+ }
+
+ final long ttlSeconds = ttl.toSeconds();
+ final int uid = clientInfo.getUid();
+
+ // Allows Thread module in the system_server to register TTL that is smaller than
+ // 30 seconds
+ final long minTtlSeconds = uid == SYSTEM_UID ? 0 : NsdManager.TTL_SECONDS_MIN;
+
+ // Allows Thread module in the system_server to register TTL that is larger than
+ // 10 hours
+ final long maxTtlSeconds =
+ uid == SYSTEM_UID ? 0xffffffffL : NsdManager.TTL_SECONDS_MAX;
+
+ if (ttlSeconds < minTtlSeconds || ttlSeconds > maxTtlSeconds) {
+ mServiceLogs.e("ttlSeconds exceeds allowed range (value = "
+ + ttlSeconds + ", allowedRange = [" + minTtlSeconds
+ + ", " + maxTtlSeconds + " ])");
+ return false;
+ }
+ return true;
+ }
+
@Override
public boolean processMessage(Message msg) {
final ClientInfo clientInfo;
@@ -746,8 +808,8 @@
switch (msg.what) {
case NsdManager.DISCOVER_SERVICES: {
if (DBG) Log.d(TAG, "Discover services");
- final ListenerArgs args = (ListenerArgs) msg.obj;
- clientInfo = mClients.get(args.connector);
+ final DiscoveryArgs discoveryArgs = (DiscoveryArgs) msg.obj;
+ clientInfo = mClients.get(discoveryArgs.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
// cleared and cause NPE. Add a null check here to prevent this corner case.
@@ -762,10 +824,10 @@
break;
}
- final NsdServiceInfo info = args.serviceInfo;
+ final DiscoveryRequest discoveryRequest = discoveryArgs.discoveryRequest;
transactionId = getUniqueId();
final Pair<String, List<String>> typeAndSubtype =
- parseTypeAndSubtype(info.getServiceType());
+ parseTypeAndSubtype(discoveryRequest.getServiceType());
final String serviceType = typeAndSubtype == null
? null : typeAndSubtype.first;
if (clientInfo.mUseJavaBackend
@@ -777,41 +839,55 @@
break;
}
+ String subtype = discoveryRequest.getSubtype();
+ if (subtype == null && !typeAndSubtype.second.isEmpty()) {
+ subtype = typeAndSubtype.second.get(0);
+ }
+
+ if (subtype != null && !checkSubtypeLabel(subtype)) {
+ clientInfo.onDiscoverServicesFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+ break;
+ }
+
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())
+ .setNetwork(discoveryRequest.getNetwork())
.setRemoveExpiredService(true)
- .setIsPassiveMode(true);
- if (!typeAndSubtype.second.isEmpty()) {
- // The parsing ensures subtype starts with an underscore.
+ .setQueryMode(
+ mMdnsFeatureFlags.isAggressiveQueryModeEnabled()
+ ? AGGRESSIVE_QUERY_MODE
+ : PASSIVE_QUERY_MODE);
+ if (subtype != null) {
+ // checkSubtypeLabels() ensures that subtypes start with '_' but
// MdnsSearchOptions expects the underscore to not be present.
- optionsBuilder.addSubtype(
- typeAndSubtype.second.get(0).substring(1));
+ optionsBuilder.addSubtype(subtype.substring(1));
}
mMdnsDiscoveryManager.registerListener(
listenServiceType, listener, optionsBuilder.build());
final ClientRequest request = storeDiscoveryManagerRequestMap(
clientRequestId, transactionId, listener, clientInfo,
- info.getNetwork());
- clientInfo.onDiscoverServicesStarted(clientRequestId, info, request);
+ discoveryRequest.getNetwork());
+ clientInfo.onDiscoverServicesStarted(
+ clientRequestId, discoveryRequest, request);
clientInfo.log("Register a DiscoveryListener " + transactionId
+ " for service type:" + listenServiceType);
} else {
maybeStartDaemon();
- if (discoverServices(transactionId, info)) {
+ if (discoverServices(transactionId, discoveryRequest)) {
if (DBG) {
Log.d(TAG, "Discover " + msg.arg2 + " " + transactionId
- + info.getServiceType());
+ + discoveryRequest.getServiceType());
}
final ClientRequest request = storeLegacyRequestMap(clientRequestId,
transactionId, clientInfo, msg.what,
mClock.elapsedRealtime());
clientInfo.onDiscoverServicesStarted(
- clientRequestId, info, request);
+ clientRequestId, discoveryRequest, request);
} else {
stopServiceDiscovery(transactionId);
clientInfo.onDiscoverServicesFailedImmediately(clientRequestId,
@@ -886,10 +962,18 @@
serviceType);
final String registerServiceType = typeSubtype == null
? null : typeSubtype.first;
+ final String hostname = serviceInfo.getHostname();
+ // Keep compatible with the legacy behavior: It's allowed to set host
+ // addresses for a service registration although the host addresses
+ // won't be registered. To register the addresses for a host, the
+ // hostname must be specified.
+ if (hostname == null) {
+ serviceInfo.setHostAddresses(Collections.emptyList());
+ }
if (clientInfo.mUseJavaBackend
|| mDeps.isMdnsAdvertiserEnabled(mContext)
|| useAdvertiserForType(registerServiceType)) {
- if (registerServiceType == null) {
+ if (serviceType != null && registerServiceType == null) {
Log.e(TAG, "Invalid service type: " + serviceType);
clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
@@ -912,14 +996,25 @@
} else {
transactionId = getUniqueId();
}
- serviceInfo.setServiceType(registerServiceType);
- serviceInfo.setServiceName(truncateServiceName(
- serviceInfo.getServiceName()));
+
+ if (registerServiceType != null) {
+ serviceInfo.setServiceType(registerServiceType);
+ serviceInfo.setServiceName(
+ truncateServiceName(serviceInfo.getServiceName()));
+ }
+
+ if (!checkHostname(hostname)) {
+ clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+ break;
+ }
Set<String> subtypes = new ArraySet<>(serviceInfo.getSubtypes());
- for (String subType: typeSubtype.second) {
- if (!TextUtils.isEmpty(subType)) {
- subtypes.add(subType);
+ if (typeSubtype != null && typeSubtype.second != null) {
+ for (String subType : typeSubtype.second) {
+ if (!TextUtils.isEmpty(subType)) {
+ subtypes.add(subType);
+ }
}
}
subtypes = dedupSubtypeLabels(subtypes);
@@ -930,15 +1025,23 @@
break;
}
+ if (!checkTtl(advertisingRequest.getTtl(), clientInfo)) {
+ clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+ break;
+ }
+
serviceInfo.setSubtypes(subtypes);
maybeStartMonitoringSockets();
final MdnsAdvertisingOptions mdnsAdvertisingOptions =
- MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(
- isUpdateOnly).build();
+ MdnsAdvertisingOptions.newBuilder()
+ .setIsOnlyUpdate(isUpdateOnly)
+ .setTtl(advertisingRequest.getTtl())
+ .build();
mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
- mdnsAdvertisingOptions);
+ mdnsAdvertisingOptions, clientInfo.mUid);
storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
- serviceInfo.getNetwork());
+ serviceInfo);
} else {
maybeStartDaemon();
transactionId = getUniqueId();
@@ -1032,10 +1135,15 @@
maybeStartMonitoringSockets();
final MdnsListener listener = new ResolutionListener(clientRequestId,
- transactionId, info, resolveServiceType);
+ transactionId, resolveServiceType, info.getServiceName());
+ final int ifaceIdx = info.getNetwork() != null
+ ? 0 : info.getInterfaceIndex();
final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
.setNetwork(info.getNetwork())
- .setIsPassiveMode(true)
+ .setInterfaceIndex(ifaceIdx)
+ .setQueryMode(mMdnsFeatureFlags.isAggressiveQueryModeEnabled()
+ ? AGGRESSIVE_QUERY_MODE
+ : PASSIVE_QUERY_MODE)
.setResolveInstanceName(info.getServiceName())
.setRemoveExpiredService(true)
.build();
@@ -1130,10 +1238,15 @@
maybeStartMonitoringSockets();
final MdnsListener listener = new ServiceInfoListener(clientRequestId,
- transactionId, info, resolveServiceType);
+ transactionId, resolveServiceType, info.getServiceName());
+ final int ifIndex = info.getNetwork() != null
+ ? 0 : info.getInterfaceIndex();
final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
.setNetwork(info.getNetwork())
- .setIsPassiveMode(true)
+ .setInterfaceIndex(ifIndex)
+ .setQueryMode(mMdnsFeatureFlags.isAggressiveQueryModeEnabled()
+ ? AGGRESSIVE_QUERY_MODE
+ : PASSIVE_QUERY_MODE)
.setResolveInstanceName(info.getServiceName())
.setRemoveExpiredService(true)
.build();
@@ -1473,6 +1586,7 @@
network == null ? INetd.LOCAL_NET_ID : network.netId,
serviceInfo.getInterfaceIndex());
servInfo.setSubtypes(dedupSubtypeLabels(serviceInfo.getSubtypes()));
+ servInfo.setExpirationTime(serviceInfo.getExpirationTime());
return servInfo;
}
@@ -1526,6 +1640,7 @@
Log.e(TAG, "Invalid attribute", e);
}
}
+ info.setHostname(getHostname(serviceInfo));
final List<InetAddress> addresses = getInetAddresses(serviceInfo);
if (addresses.size() != 0) {
info.setHostAddresses(addresses);
@@ -1562,6 +1677,7 @@
}
}
+ info.setHostname(getHostname(serviceInfo));
final List<InetAddress> addresses = getInetAddresses(serviceInfo);
info.setHostAddresses(addresses);
clientInfo.onServiceUpdated(clientRequestId, info, request);
@@ -1608,6 +1724,16 @@
return addresses;
}
+ @NonNull
+ private static String getHostname(@NonNull MdnsServiceInfo serviceInfo) {
+ String[] hostname = serviceInfo.getHostName();
+ // Strip the "local" top-level domain.
+ if (hostname.length >= 2 && hostname[hostname.length - 1].equals("local")) {
+ hostname = Arrays.copyOf(hostname, hostname.length - 1);
+ }
+ return String.join(".", hostname);
+ }
+
private static void setServiceNetworkForCallback(NsdServiceInfo info, int netId, int ifaceIdx) {
switch (netId) {
case NETID_UNSET:
@@ -1693,9 +1819,24 @@
return new Pair<>(queryType, Collections.emptyList());
}
+ /**
+ * Checks if the hostname is valid.
+ *
+ * <p>For now NsdService only allows single-label hostnames conforming to RFC 1035. In other
+ * words, the hostname should be at most 63 characters long and it only contains letters, digits
+ * and hyphens.
+ */
+ public static boolean checkHostname(@Nullable String hostname) {
+ if (hostname == null) {
+ return true;
+ }
+ String HOSTNAME_REGEX = "^[a-zA-Z]([a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$";
+ return Pattern.compile(HOSTNAME_REGEX).matcher(hostname).matches();
+ }
+
/** Returns {@code true} if {@code subtype} is a valid DNS-SD subtype label. */
private static boolean checkSubtypeLabel(String subtype) {
- return Pattern.compile("^" + TYPE_SUBTYPE_LABEL_REGEX + "$").matcher(subtype).matches();
+ return Pattern.compile("^" + SUBTYPE_LABEL_REGEX + "$").matcher(subtype).matches();
}
@VisibleForTesting
@@ -1732,7 +1873,7 @@
am.addOnUidImportanceListener(new UidImportanceListener(handler),
mRunningAppActiveImportanceCutoff);
- final MdnsFeatureFlags flags = new MdnsFeatureFlags.Builder()
+ mMdnsFeatureFlags = new MdnsFeatureFlags.Builder()
.setIsMdnsOffloadFeatureEnabled(mDeps.isTetheringFeatureNotChickenedOut(
mContext, MdnsFeatureFlags.NSD_FORCE_DISABLE_MDNS_OFFLOAD))
.setIncludeInetAddressRecordsInProbing(mDeps.isFeatureEnabled(
@@ -1743,15 +1884,25 @@
mContext, MdnsFeatureFlags.NSD_LIMIT_LABEL_COUNT))
.setIsKnownAnswerSuppressionEnabled(mDeps.isFeatureEnabled(
mContext, MdnsFeatureFlags.NSD_KNOWN_ANSWER_SUPPRESSION))
+ .setIsUnicastReplyEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_UNICAST_REPLY_ENABLED))
+ .setIsAggressiveQueryModeEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_AGGRESSIVE_QUERY_MODE))
+ .setIsQueryWithKnownAnswerEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_QUERY_WITH_KNOWN_ANSWER))
+ .setOverrideProvider(flag -> mDeps.isFeatureEnabled(
+ mContext, FORCE_ENABLE_FLAG_FOR_TEST_PREFIX + flag))
.build();
mMdnsSocketClient =
new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider,
- LOGGER.forSubComponent("MdnsMultinetworkSocketClient"), flags);
+ LOGGER.forSubComponent("MdnsMultinetworkSocketClient"), mMdnsFeatureFlags);
mMdnsDiscoveryManager = deps.makeMdnsDiscoveryManager(new ExecutorProvider(),
- mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"), flags);
+ mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"),
+ mMdnsFeatureFlags);
handler.post(() -> mMdnsSocketClient.setCallback(mMdnsDiscoveryManager));
mAdvertiser = deps.makeMdnsAdvertiser(handler.getLooper(), mMdnsSocketProvider,
- new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"), flags);
+ new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"),
+ mMdnsFeatureFlags, mContext);
mClock = deps.makeClock();
}
@@ -1807,13 +1958,6 @@
}
/**
- * @see DeviceConfigUtils#isTrunkStableFeatureEnabled
- */
- public boolean isTrunkStableFeatureEnabled(String feature) {
- return DeviceConfigUtils.isTrunkStableFeatureEnabled(feature);
- }
-
- /**
* @see MdnsDiscoveryManager
*/
public MdnsDiscoveryManager makeMdnsDiscoveryManager(
@@ -1830,8 +1974,8 @@
public MdnsAdvertiser makeMdnsAdvertiser(
@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
@NonNull MdnsAdvertiser.AdvertiserCallback cb, @NonNull SharedLog sharedLog,
- MdnsFeatureFlags featureFlags) {
- return new MdnsAdvertiser(looper, socketProvider, cb, sharedLog, featureFlags);
+ MdnsFeatureFlags featureFlags, Context context) {
+ return new MdnsAdvertiser(looper, socketProvider, cb, sharedLog, featureFlags, context);
}
/**
@@ -2017,9 +2161,10 @@
final int clientRequestId = getClientRequestIdOrLog(clientInfo, transactionId);
if (clientRequestId < 0) return;
- // onRegisterServiceSucceeded only has the service name in its info. This aligns with
- // historical behavior.
+ // onRegisterServiceSucceeded only has the service name and hostname in its info. This
+ // aligns with historical behavior.
final NsdServiceInfo cbInfo = new NsdServiceInfo(registeredInfo.getServiceName(), null);
+ cbInfo.setHostname(registeredInfo.getHostname());
final ClientRequest request = clientInfo.mClientRequests.get(clientRequestId);
clientInfo.onRegisterServiceSucceeded(clientRequestId, cbInfo, request);
}
@@ -2114,12 +2259,22 @@
}
}
+ private static final class DiscoveryArgs {
+ public final NsdServiceConnector connector;
+ public final DiscoveryRequest discoveryRequest;
+ DiscoveryArgs(NsdServiceConnector connector, DiscoveryRequest discoveryRequest) {
+ this.connector = connector;
+ this.discoveryRequest = discoveryRequest;
+ }
+ }
+
private class NsdServiceConnector extends INsdServiceConnector.Stub
implements IBinder.DeathRecipient {
@Override
public void registerService(int listenerKey, AdvertisingRequest advertisingRequest)
throws RemoteException {
+ NsdManager.checkServiceInfoForRegistration(advertisingRequest.getServiceInfo());
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.REGISTER_SERVICE, 0, listenerKey,
new AdvertisingArgs(this, advertisingRequest)
@@ -2134,10 +2289,10 @@
}
@Override
- public void discoverServices(int listenerKey, NsdServiceInfo serviceInfo) {
+ public void discoverServices(int listenerKey, DiscoveryRequest discoveryRequest) {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.DISCOVER_SERVICES, 0, listenerKey,
- new ListenerArgs(this, serviceInfo)));
+ new DiscoveryArgs(this, discoveryRequest)));
}
@Override
@@ -2224,7 +2379,7 @@
permissionsList.add(DEVICE_POWER);
}
- if (PermissionUtils.checkAnyPermissionOf(context,
+ if (PermissionUtils.hasAnyPermissionOf(context,
permissionsList.toArray(new String[0]))) {
return;
}
@@ -2276,15 +2431,15 @@
return mMDnsManager.stopOperation(transactionId);
}
- private boolean discoverServices(int transactionId, NsdServiceInfo serviceInfo) {
+ private boolean discoverServices(int transactionId, DiscoveryRequest discoveryRequest) {
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) {
+ final String type = discoveryRequest.getServiceType();
+ final int discoverInterface = getNetworkInterfaceIndex(discoveryRequest);
+ if (discoveryRequest.getNetwork() != null && discoverInterface == IFACE_IDX_ANY) {
Log.e(TAG, "Interface to discover service on not found");
return false;
}
@@ -2334,7 +2489,26 @@
}
return IFACE_IDX_ANY;
}
+ return getNetworkInterfaceIndex(network);
+ }
+ /**
+ * Returns the interface to use to discover a service on a specific network, or {@link
+ * IFACE_IDX_ANY} if no network is specified.
+ */
+ private int getNetworkInterfaceIndex(DiscoveryRequest discoveryRequest) {
+ final Network network = discoveryRequest.getNetwork();
+ if (network == null) {
+ return IFACE_IDX_ANY;
+ }
+ return getNetworkInterfaceIndex(network);
+ }
+
+ /**
+ * Returns the interface of a specific network, or {@link IFACE_IDX_ANY} if no interface is
+ * associated with {@code network}.
+ */
+ private int getNetworkInterfaceIndex(@NonNull Network network) {
String interfaceName = getNetworkInterfaceName(network);
if (interfaceName == null) {
return IFACE_IDX_ANY;
@@ -2402,18 +2576,37 @@
@Override
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
- if (!PermissionUtils.checkDumpPermission(mContext, TAG, writer)) return;
+ if (!PermissionUtils.hasDumpPermission(mContext, TAG, writer)) return;
final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
// Dump state machine logs
mNsdStateMachine.dump(fd, pw, args);
+ // Dump clients
+ pw.println();
+ pw.println("Active clients:");
+ pw.increaseIndent();
+ HandlerUtils.runWithScissorsForDump(mNsdStateMachine.getHandler(), () -> {
+ for (ClientInfo clientInfo : mClients.values()) {
+ pw.println(clientInfo.toString());
+ }
+ }, 10_000);
+ pw.decreaseIndent();
+
// Dump service and clients logs
pw.println();
pw.println("Logs:");
pw.increaseIndent();
mServiceLogs.reverseDump(pw);
pw.decreaseIndent();
+
+ //Dump DiscoveryManager
+ pw.println();
+ pw.println("DiscoveryManager:");
+ pw.increaseIndent();
+ HandlerUtils.runWithScissorsForDump(
+ mNsdStateMachine.getHandler(), () -> mMdnsDiscoveryManager.dump(pw), 10_000);
+ pw.decreaseIndent();
}
private abstract static class ClientRequest {
@@ -2472,6 +2665,21 @@
public int getSentQueryCount() {
return mSentQueryCount;
}
+
+ @NonNull
+ @Override
+ public String toString() {
+ return getRequestDescriptor() + " {" + mTransactionId
+ + ", startTime " + mStartTimeMs
+ + ", foundServices " + mFoundServiceCount
+ + ", lostServices " + mLostServiceCount
+ + ", fromCache " + mIsServiceFromCache
+ + ", sentQueries " + mSentQueryCount
+ + "}";
+ }
+
+ @NonNull
+ protected abstract String getRequestDescriptor();
}
private static class LegacyClientRequest extends ClientRequest {
@@ -2481,6 +2689,12 @@
super(transactionId, startTimeMs);
mRequestCode = requestCode;
}
+
+ @NonNull
+ @Override
+ protected String getRequestDescriptor() {
+ return "Legacy (" + mRequestCode + ")";
+ }
}
private abstract static class JavaBackendClientRequest extends ClientRequest {
@@ -2500,9 +2714,20 @@
}
private static class AdvertiserClientRequest extends JavaBackendClientRequest {
+ @NonNull
+ private final String mServiceFullName;
+
private AdvertiserClientRequest(int transactionId, @Nullable Network requestedNetwork,
- long startTimeMs) {
+ @NonNull String serviceFullName, long startTimeMs) {
super(transactionId, requestedNetwork, startTimeMs);
+ mServiceFullName = serviceFullName;
+ }
+
+ @NonNull
+ @Override
+ public String getRequestDescriptor() {
+ return String.format("Advertiser: serviceFullName=%s, net=%s",
+ mServiceFullName, getRequestedNetwork());
}
}
@@ -2515,12 +2740,26 @@
super(transactionId, requestedNetwork, startTimeMs);
mListener = listener;
}
+
+ @NonNull
+ @Override
+ public String getRequestDescriptor() {
+ return String.format("Discovery/%s, net=%s", mListener, getRequestedNetwork());
+ }
}
/* Information tracked per client */
private class ClientInfo {
- private static final int MAX_LIMIT = 10;
+ /**
+ * Maximum number of requests (callbacks) for a client.
+ *
+ * 200 listeners should be more than enough for most use-cases: even if a client tries to
+ * file callbacks for every service on a local network, there are generally much less than
+ * 200 devices on a local network (a /24 only allows 255 IPv4 devices), and while some
+ * devices may have multiple services, many devices do not advertise any.
+ */
+ private static final int MAX_LIMIT = 200;
private final INsdManagerCallback mCb;
/* Remembers a resolved service until getaddrinfo completes */
private NsdServiceInfo mResolvedService;
@@ -2551,22 +2790,24 @@
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
- sb.append("mResolvedService ").append(mResolvedService).append("\n");
- sb.append("mIsLegacy ").append(mIsPreSClient).append("\n");
- sb.append("mUseJavaBackend ").append(mUseJavaBackend).append("\n");
- sb.append("mUid ").append(mUid).append("\n");
+ sb.append("mUid ").append(mUid).append(", ");
+ sb.append("mResolvedService ").append(mResolvedService).append(", ");
+ sb.append("mIsLegacy ").append(mIsPreSClient).append(", ");
+ sb.append("mUseJavaBackend ").append(mUseJavaBackend).append(", ");
+ sb.append("mClientRequests:\n");
for (int i = 0; i < mClientRequests.size(); i++) {
int clientRequestId = mClientRequests.keyAt(i);
- sb.append("clientRequestId ")
- .append(clientRequestId)
- .append(" transactionId ").append(mClientRequests.valueAt(i).mTransactionId)
- .append(" type ").append(
- mClientRequests.valueAt(i).getClass().getSimpleName())
+ sb.append(" ").append(clientRequestId)
+ .append(": ").append(mClientRequests.valueAt(i).toString())
.append("\n");
}
return sb.toString();
}
+ public int getUid() {
+ return mUid;
+ }
+
private boolean isPreSClient() {
return mIsPreSClient;
}
@@ -2709,12 +2950,12 @@
&& !(request instanceof AdvertiserClientRequest);
}
- void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info,
+ void onDiscoverServicesStarted(int listenerKey, DiscoveryRequest discoveryRequest,
ClientRequest request) {
mMetrics.reportServiceDiscoveryStarted(
isLegacyClientRequest(request), request.mTransactionId);
try {
- mCb.onDiscoverServicesStarted(listenerKey, info);
+ mCb.onDiscoverServicesStarted(listenerKey, discoveryRequest);
} catch (RemoteException e) {
Log.e(TAG, "Error calling onDiscoverServicesStarted", e);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
index c4d3338..54943c7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -23,6 +23,7 @@
import android.text.TextUtils;
import android.util.Pair;
+import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -63,8 +64,6 @@
@NonNull
private final WeakReference<MdnsSocketClientBase> weakRequestSender;
@NonNull
- private final MdnsPacketWriter packetWriter;
- @NonNull
private final String[] serviceTypeLabels;
@NonNull
private final List<String> subtypes;
@@ -79,11 +78,16 @@
private final MdnsUtils.Clock clock;
@NonNull
private final SharedLog sharedLog;
+ @NonNull
+ private final MdnsServiceTypeClient.Dependencies dependencies;
private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
+ private final byte[] packetCreationBuffer = new byte[1500]; // TODO: use interface MTU
+ @NonNull
+ private final List<MdnsResponse> existingServices;
+ private final boolean isQueryWithKnownAnswer;
EnqueueMdnsQueryCallable(
@NonNull MdnsSocketClientBase requestSender,
- @NonNull MdnsPacketWriter packetWriter,
@NonNull String serviceType,
@NonNull Collection<String> subtypes,
boolean expectUnicastResponse,
@@ -93,9 +97,11 @@
boolean sendDiscoveryQueries,
@NonNull Collection<MdnsResponse> servicesToResolve,
@NonNull MdnsUtils.Clock clock,
- @NonNull SharedLog sharedLog) {
+ @NonNull SharedLog sharedLog,
+ @NonNull MdnsServiceTypeClient.Dependencies dependencies,
+ @NonNull Collection<MdnsResponse> existingServices,
+ boolean isQueryWithKnownAnswer) {
weakRequestSender = new WeakReference<>(requestSender);
- this.packetWriter = packetWriter;
serviceTypeLabels = TextUtils.split(serviceType, "\\.");
this.subtypes = new ArrayList<>(subtypes);
this.expectUnicastResponse = expectUnicastResponse;
@@ -106,6 +112,9 @@
this.servicesToResolve = new ArrayList<>(servicesToResolve);
this.clock = clock;
this.sharedLog = sharedLog;
+ this.dependencies = dependencies;
+ this.existingServices = new ArrayList<>(existingServices);
+ this.isQueryWithKnownAnswer = isQueryWithKnownAnswer;
}
/**
@@ -176,62 +185,86 @@
return Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>());
}
+ // Put the existing ptr records into known-answer section.
+ final List<MdnsRecord> knownAnswers = new ArrayList<>();
+ if (sendDiscoveryQueries) {
+ for (MdnsResponse existingService : existingServices) {
+ for (MdnsPointerRecord ptrRecord : existingService.getPointerRecords()) {
+ // Ignore any PTR records that don't match the current query.
+ if (!CollectionUtils.any(questions,
+ q -> q instanceof MdnsPointerRecord
+ && MdnsUtils.equalsDnsLabelIgnoreDnsCase(
+ q.getName(), ptrRecord.getName()))) {
+ continue;
+ }
+
+ knownAnswers.add(new MdnsPointerRecord(
+ ptrRecord.getName(),
+ ptrRecord.getReceiptTime(),
+ ptrRecord.getCacheFlush(),
+ ptrRecord.getRemainingTTL(now), // Put the remaining ttl.
+ ptrRecord.getPointer()));
+ }
+ }
+ }
+
final MdnsPacket queryPacket = new MdnsPacket(
transactionId,
MdnsConstants.FLAGS_QUERY,
questions,
- Collections.emptyList(), /* answers */
+ knownAnswers,
Collections.emptyList(), /* authorityRecords */
Collections.emptyList() /* additionalRecords */);
- MdnsUtils.writeMdnsPacket(packetWriter, queryPacket);
- sendPacketToIpv4AndIpv6(requestSender, MdnsConstants.MDNS_PORT);
+ sendPacketToIpv4AndIpv6(requestSender, MdnsConstants.MDNS_PORT, queryPacket);
for (Integer emulatorPort : castShellEmulatorMdnsPorts) {
- sendPacketToIpv4AndIpv6(requestSender, emulatorPort);
+ sendPacketToIpv4AndIpv6(requestSender, emulatorPort, queryPacket);
}
return Pair.create(transactionId, subtypes);
- } catch (IOException e) {
+ } catch (Exception e) {
sharedLog.e(String.format("Failed to create mDNS packet for subtype: %s.",
TextUtils.join(",", subtypes)), e);
return Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>());
}
}
- private void sendPacket(MdnsSocketClientBase requestSender, InetSocketAddress address)
- throws IOException {
- DatagramPacket packet = packetWriter.getPacket(address);
+ private void sendPacket(MdnsSocketClientBase requestSender, InetSocketAddress address,
+ MdnsPacket mdnsPacket) throws IOException {
+ final List<DatagramPacket> packets = dependencies.getDatagramPacketsFromMdnsPacket(
+ packetCreationBuffer, mdnsPacket, address, isQueryWithKnownAnswer);
if (expectUnicastResponse) {
// MdnsMultinetworkSocketClient is only available on T+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
&& requestSender instanceof MdnsMultinetworkSocketClient) {
((MdnsMultinetworkSocketClient) requestSender).sendPacketRequestingUnicastResponse(
- packet, socketKey, onlyUseIpv6OnIpv6OnlyNetworks);
+ packets, socketKey, onlyUseIpv6OnIpv6OnlyNetworks);
} else {
requestSender.sendPacketRequestingUnicastResponse(
- packet, onlyUseIpv6OnIpv6OnlyNetworks);
+ packets, onlyUseIpv6OnIpv6OnlyNetworks);
}
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
&& requestSender instanceof MdnsMultinetworkSocketClient) {
((MdnsMultinetworkSocketClient) requestSender)
.sendPacketRequestingMulticastResponse(
- packet, socketKey, onlyUseIpv6OnIpv6OnlyNetworks);
+ packets, socketKey, onlyUseIpv6OnIpv6OnlyNetworks);
} else {
requestSender.sendPacketRequestingMulticastResponse(
- packet, onlyUseIpv6OnIpv6OnlyNetworks);
+ packets, onlyUseIpv6OnIpv6OnlyNetworks);
}
}
}
- private void sendPacketToIpv4AndIpv6(MdnsSocketClientBase requestSender, int port) {
+ private void sendPacketToIpv4AndIpv6(MdnsSocketClientBase requestSender, int port,
+ MdnsPacket mdnsPacket) {
try {
sendPacket(requestSender,
- new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), port));
+ new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), port), mdnsPacket);
} catch (IOException e) {
sharedLog.e("Can't send packet to IPv4", e);
}
try {
sendPacket(requestSender,
- new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), port));
+ new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), port), mdnsPacket);
} catch (IOException e) {
sharedLog.e("Can't send packet to IPv6", e);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index 135d957..98c2d86 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -17,11 +17,14 @@
package com.android.server.connectivity.mdns;
import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
+import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_HOST;
+import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE;
import static com.android.server.connectivity.mdns.MdnsRecord.MAX_LABEL_LENGTH;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresApi;
+import android.content.Context;
import android.net.LinkAddress;
import android.net.Network;
import android.net.nsd.NsdManager;
@@ -30,13 +33,16 @@
import android.net.nsd.OffloadServiceInfo;
import android.os.Build;
import android.os.Looper;
+import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.SparseArray;
+import com.android.connectivity.resources.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.ConnectivityResources;
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.util.ArrayList;
@@ -84,6 +90,7 @@
private final Map<String, List<OffloadServiceInfoWrapper>> mInterfaceOffloadServices =
new ArrayMap<>();
private final MdnsFeatureFlags mMdnsFeatureFlags;
+ private final Map<String, Integer> mServiceTypeToOffloadPriority;
/**
* Dependencies for {@link MdnsAdvertiser}, useful for testing.
@@ -147,7 +154,9 @@
mSharedLog.wtf("Register succeeded for unknown registration");
return;
}
- if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled) {
+ if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled
+ // TODO: Enable offload when the serviceInfo contains a custom host.
+ && TextUtils.isEmpty(registration.getServiceInfo().getHostname())) {
final String interfaceName = advertiser.getSocketInterfaceName();
final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers =
mInterfaceOffloadServices.computeIfAbsent(interfaceName,
@@ -175,8 +184,11 @@
}
@Override
- public void onServiceConflict(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId) {
- mSharedLog.i("Found conflict, restarted probing for service " + serviceId);
+ public void onServiceConflict(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId,
+ int conflictType) {
+ mSharedLog.i("Found conflict, restarted probing for service "
+ + serviceId + " "
+ + conflictType);
final Registration registration = mRegistrations.get(serviceId);
if (registration == null) return;
@@ -201,10 +213,22 @@
return;
}
- // Conflict was found during probing; rename once to find a name that has no conflict
- registration.updateForConflict(
- registration.makeNewServiceInfoForConflict(1 /* renameCount */),
- 1 /* renameCount */);
+ if ((conflictType & CONFLICT_SERVICE) != 0) {
+ // Service conflict was found during probing; rename once to find a name that has no
+ // conflict
+ registration.updateForServiceConflict(
+ registration.makeNewServiceInfoForServiceConflict(1 /* renameCount */),
+ 1 /* renameCount */);
+ }
+
+ if ((conflictType & CONFLICT_HOST) != 0) {
+ // Host conflict was found during probing; rename once to find a name that has no
+ // conflict
+ registration.updateForHostConflict(
+ registration.makeNewServiceInfoForHostConflict(1 /* renameCount */),
+ 1 /* renameCount */);
+ }
+
registration.mConflictDuringProbingCount++;
// Keep renaming if the new name conflicts in local registrations
@@ -217,33 +241,61 @@
}
@Override
- public void onDestroyed(@NonNull MdnsInterfaceSocket socket) {
- for (int i = mAdvertiserRequests.size() - 1; i >= 0; i--) {
- if (mAdvertiserRequests.valueAt(i).onAdvertiserDestroyed(socket)) {
- mAdvertiserRequests.removeAt(i);
- }
- }
- mAllAdvertisers.remove(socket);
+ public void onAllServicesRemoved(@NonNull MdnsInterfaceSocket socket) {
+ if (DBG) { mSharedLog.i("onAllServicesRemoved: " + socket); }
+ // Try destroying the advertiser if all services has been removed
+ destroyAdvertiser(socket, false /* interfaceDestroyed */);
}
};
- private boolean hasAnyConflict(
+ private boolean hasAnyServiceConflict(
@NonNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter,
- @NonNull NsdServiceInfo newInfo) {
- return any(mAdvertiserRequests, (network, adv) ->
- applicableAdvertiserFilter.test(network, adv) && adv.hasConflict(newInfo));
+ @NonNull NsdServiceInfo newInfo,
+ @NonNull Registration originalRegistration) {
+ return any(
+ mAdvertiserRequests,
+ (network, adv) ->
+ applicableAdvertiserFilter.test(network, adv)
+ && adv.hasServiceConflict(newInfo, originalRegistration));
+ }
+
+ private boolean hasAnyHostConflict(
+ @NonNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter,
+ @NonNull NsdServiceInfo newInfo,
+ int clientUid) {
+ // Check if it conflicts with custom hosts.
+ if (any(
+ mAdvertiserRequests,
+ (network, adv) ->
+ applicableAdvertiserFilter.test(network, adv)
+ && adv.hasHostConflict(newInfo, clientUid))) {
+ return true;
+ }
+ // Check if it conflicts with the default hostname.
+ return MdnsUtils.equalsIgnoreDnsCase(newInfo.getHostname(), mDeviceHostName[0]);
}
private void updateRegistrationUntilNoConflict(
@NonNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter,
@NonNull Registration registration) {
- int renameCount = 0;
NsdServiceInfo newInfo = registration.getServiceInfo();
- while (hasAnyConflict(applicableAdvertiserFilter, newInfo)) {
- renameCount++;
- newInfo = registration.makeNewServiceInfoForConflict(renameCount);
+
+ int renameServiceCount = 0;
+ while (hasAnyServiceConflict(applicableAdvertiserFilter, newInfo, registration)) {
+ renameServiceCount++;
+ newInfo = registration.makeNewServiceInfoForServiceConflict(renameServiceCount);
}
- registration.updateForConflict(newInfo, renameCount);
+ registration.updateForServiceConflict(newInfo, renameServiceCount);
+
+ if (!TextUtils.isEmpty(registration.getServiceInfo().getHostname())) {
+ int renameHostCount = 0;
+ while (hasAnyHostConflict(
+ applicableAdvertiserFilter, newInfo, registration.mClientUid)) {
+ renameHostCount++;
+ newInfo = registration.makeNewServiceInfoForHostConflict(renameHostCount);
+ }
+ registration.updateForHostConflict(newInfo, renameHostCount);
+ }
}
private void maybeSendOffloadStop(final String interfaceName, int serviceId) {
@@ -263,6 +315,30 @@
}
/**
+ * Destroys the advertiser for the interface indicated by {@code socket}.
+ *
+ * {@code interfaceDestroyed} should be set to {@code true} if this method is called because
+ * the associated interface has been destroyed.
+ */
+ private void destroyAdvertiser(MdnsInterfaceSocket socket, boolean interfaceDestroyed) {
+ InterfaceAdvertiserRequest advertiserRequest;
+
+ MdnsInterfaceAdvertiser advertiser = mAllAdvertisers.remove(socket);
+ if (advertiser != null) {
+ advertiser.destroyNow();
+ if (DBG) { mSharedLog.i("MdnsInterfaceAdvertiser is destroyed: " + advertiser); }
+ }
+
+ for (int i = mAdvertiserRequests.size() - 1; i >= 0; i--) {
+ advertiserRequest = mAdvertiserRequests.valueAt(i);
+ if (advertiserRequest.onAdvertiserDestroyed(socket, interfaceDestroyed)) {
+ if (DBG) { mSharedLog.i("AdvertiserRequest is removed: " + advertiserRequest); }
+ mAdvertiserRequests.removeAt(i);
+ }
+ }
+ }
+
+ /**
* A request for a {@link MdnsInterfaceAdvertiser}.
*
* This class tracks services to be advertised on all sockets provided via a registered
@@ -281,13 +357,22 @@
}
/**
- * Called when an advertiser was destroyed, after all services were unregistered and it sent
- * exit announcements, or the interface is gone.
+ * Called when the interface advertiser associated with {@code socket} has been destroyed.
*
- * @return true if this {@link InterfaceAdvertiserRequest} should now be deleted.
+ * {@code interfaceDestroyed} should be set to {@code true} if this method is called because
+ * the associated interface has been destroyed.
+ *
+ * @return true if the {@link InterfaceAdvertiserRequest} should now be deleted
*/
- boolean onAdvertiserDestroyed(@NonNull MdnsInterfaceSocket socket) {
+ boolean onAdvertiserDestroyed(
+ @NonNull MdnsInterfaceSocket socket, boolean interfaceDestroyed) {
final MdnsInterfaceAdvertiser removedAdvertiser = mAdvertisers.remove(socket);
+ if (removedAdvertiser != null
+ && !interfaceDestroyed && mPendingRegistrations.size() > 0) {
+ mSharedLog.wtf(
+ "unexpected onAdvertiserDestroyed() when there are pending registrations");
+ }
+
if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled && removedAdvertiser != null) {
final String interfaceName = removedAdvertiser.getSocketInterfaceName();
// If the interface is destroyed, stop all hardware offloading on that
@@ -322,17 +407,34 @@
/**
* Return whether using the proposed new {@link NsdServiceInfo} to add a registration would
- * cause a conflict in this {@link InterfaceAdvertiserRequest}.
+ * cause a conflict of the service in this {@link InterfaceAdvertiserRequest}.
*/
- boolean hasConflict(@NonNull NsdServiceInfo newInfo) {
- return getConflictingService(newInfo) >= 0;
+ boolean hasServiceConflict(
+ @NonNull NsdServiceInfo newInfo, @NonNull Registration originalRegistration) {
+ return getConflictingRegistrationDueToService(newInfo, originalRegistration) >= 0;
}
/**
- * Get the ID of a conflicting service, or -1 if none.
+ * Return whether using the proposed new {@link NsdServiceInfo} to add a registration would
+ * cause a conflict of the host in this {@link InterfaceAdvertiserRequest}.
+ *
+ * @param clientUid UID of the user who wants to advertise the serviceInfo.
*/
- int getConflictingService(@NonNull NsdServiceInfo info) {
+ boolean hasHostConflict(@NonNull NsdServiceInfo newInfo, int clientUid) {
+ return getConflictingRegistrationDueToHost(newInfo, clientUid) >= 0;
+ }
+
+ /** Get the ID of a conflicting registration due to service, or -1 if none. */
+ int getConflictingRegistrationDueToService(
+ @NonNull NsdServiceInfo info, @NonNull Registration originalRegistration) {
+ if (TextUtils.isEmpty(info.getServiceName())) {
+ return -1;
+ }
for (int i = 0; i < mPendingRegistrations.size(); i++) {
+ // Never conflict with itself
+ if (mPendingRegistrations.valueAt(i) == originalRegistration) {
+ continue;
+ }
final NsdServiceInfo other = mPendingRegistrations.valueAt(i).getServiceInfo();
if (MdnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
&& MdnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
@@ -344,15 +446,41 @@
}
/**
+ * Get the ID of a conflicting registration due to host, or -1 if none.
+ *
+ * <p>It's valid that multiple registrations from the same user are using the same hostname.
+ *
+ * <p>If there's already another registration with the same hostname requested by another
+ * user, this is considered a conflict.
+ */
+ int getConflictingRegistrationDueToHost(@NonNull NsdServiceInfo info, int clientUid) {
+ if (TextUtils.isEmpty(info.getHostname())) {
+ return -1;
+ }
+ for (int i = 0; i < mPendingRegistrations.size(); i++) {
+ final Registration otherRegistration = mPendingRegistrations.valueAt(i);
+ final NsdServiceInfo otherInfo = otherRegistration.getServiceInfo();
+ if (clientUid != otherRegistration.mClientUid
+ && MdnsUtils.equalsIgnoreDnsCase(
+ info.getHostname(), otherInfo.getHostname())) {
+ return mPendingRegistrations.keyAt(i);
+ }
+ }
+ return -1;
+ }
+
+ /**
* Add a service to advertise.
*
- * Conflicts must be checked via {@link #getConflictingService} before attempting to add.
+ * <p>Conflicts must be checked via {@link #getConflictingRegistrationDueToService} and
+ * {@link #getConflictingRegistrationDueToHost} before attempting to add.
*/
void addService(int id, @NonNull Registration registration) {
mPendingRegistrations.put(id, registration);
for (int i = 0; i < mAdvertisers.size(); i++) {
try {
- mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo());
+ mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo(),
+ registration.getAdvertisingOptions());
} catch (NameConflictException e) {
mSharedLog.wtf("Name conflict adding services that should have unique names",
e);
@@ -418,7 +546,7 @@
final Registration registration = mPendingRegistrations.valueAt(i);
try {
advertiser.addService(mPendingRegistrations.keyAt(i),
- registration.getServiceInfo());
+ registration.getServiceInfo(), registration.getAdvertisingOptions());
} catch (NameConflictException e) {
mSharedLog.wtf("Name conflict adding services that should have unique names",
e);
@@ -430,7 +558,7 @@
public void onInterfaceDestroyed(@NonNull SocketKey socketKey,
@NonNull MdnsInterfaceSocket socket) {
final MdnsInterfaceAdvertiser advertiser = mAdvertisers.get(socket);
- if (advertiser != null) advertiser.destroyNow();
+ if (advertiser != null) destroyAdvertiser(socket, true /* interfaceDestroyed */);
}
@Override
@@ -480,27 +608,37 @@
}
private static class Registration {
- @NonNull
- final String mOriginalName;
+ @Nullable
+ final String mOriginalServiceName;
+ @Nullable
+ final String mOriginalHostname;
boolean mNotifiedRegistrationSuccess;
- private int mConflictCount;
+ private int mServiceNameConflictCount;
+ private int mHostnameConflictCount;
@NonNull
private NsdServiceInfo mServiceInfo;
+ final int mClientUid;
+ private final MdnsAdvertisingOptions mAdvertisingOptions;
int mConflictDuringProbingCount;
int mConflictAfterProbingCount;
- private Registration(@NonNull NsdServiceInfo serviceInfo) {
- this.mOriginalName = serviceInfo.getServiceName();
+ private Registration(@NonNull NsdServiceInfo serviceInfo, int clientUid,
+ @NonNull MdnsAdvertisingOptions advertisingOptions) {
+ this.mOriginalServiceName = serviceInfo.getServiceName();
+ this.mOriginalHostname = serviceInfo.getHostname();
this.mServiceInfo = serviceInfo;
+ this.mClientUid = clientUid;
+ this.mAdvertisingOptions = advertisingOptions;
}
- /**
- * Matches between the NsdServiceInfo in the Registration and the provided argument.
- */
- public boolean matches(@Nullable NsdServiceInfo newInfo) {
- return Objects.equals(newInfo.getServiceName(), mOriginalName) && Objects.equals(
- newInfo.getServiceType(), mServiceInfo.getServiceType()) && Objects.equals(
- newInfo.getNetwork(), mServiceInfo.getNetwork());
+ /** Check if the new {@link NsdServiceInfo} doesn't update any data other than subtypes. */
+ public boolean isSubtypeOnlyUpdate(@NonNull NsdServiceInfo newInfo) {
+ return Objects.equals(newInfo.getServiceName(), mOriginalServiceName)
+ && Objects.equals(newInfo.getServiceType(), mServiceInfo.getServiceType())
+ && newInfo.getPort() == mServiceInfo.getPort()
+ && Objects.equals(newInfo.getHostname(), mOriginalHostname)
+ && Objects.equals(newInfo.getHostAddresses(), mServiceInfo.getHostAddresses())
+ && Objects.equals(newInfo.getNetwork(), mServiceInfo.getNetwork());
}
/**
@@ -517,8 +655,19 @@
* @param newInfo New service info to use.
* @param renameCount How many renames were done before reaching the current name.
*/
- private void updateForConflict(@NonNull NsdServiceInfo newInfo, int renameCount) {
- mConflictCount += renameCount;
+ private void updateForServiceConflict(@NonNull NsdServiceInfo newInfo, int renameCount) {
+ mServiceNameConflictCount += renameCount;
+ mServiceInfo = newInfo;
+ }
+
+ /**
+ * Update the registration to use a different host name, after a conflict was found.
+ *
+ * @param newInfo New service info to use.
+ * @param renameCount How many renames were done before reaching the current name.
+ */
+ private void updateForHostConflict(@NonNull NsdServiceInfo newInfo, int renameCount) {
+ mHostnameConflictCount += renameCount;
mServiceInfo = newInfo;
}
@@ -534,7 +683,7 @@
* @param renameCount How much to increase the number suffix for this conflict.
*/
@NonNull
- public NsdServiceInfo makeNewServiceInfoForConflict(int renameCount) {
+ public NsdServiceInfo makeNewServiceInfoForServiceConflict(int renameCount) {
// In case of conflict choose a different service name. After the first conflict use
// "Name (2)", then "Name (3)" etc.
// TODO: use a hidden method in NsdServiceInfo once MdnsAdvertiser is moved to service-t
@@ -543,17 +692,49 @@
return newInfo;
}
+ /**
+ * Make a new hostname for the registration, after a conflict was found.
+ *
+ * <p>If a name conflict was found during probing or because different advertising requests
+ * used the same name, the registration is attempted again with a new name (here using a
+ * number suffix, -1, -2, etc). Registration success is notified once probing succeeds with
+ * a new name.
+ *
+ * @param renameCount How much to increase the number suffix for this conflict.
+ */
+ @NonNull
+ public NsdServiceInfo makeNewServiceInfoForHostConflict(int renameCount) {
+ // In case of conflict choose a different hostname. After the first conflict use
+ // "Name-2", then "Name-3" etc.
+ final NsdServiceInfo newInfo = new NsdServiceInfo(mServiceInfo);
+ newInfo.setHostname(getUpdatedHostname(renameCount));
+ return newInfo;
+ }
+
private String getUpdatedServiceName(int renameCount) {
- final String suffix = " (" + (mConflictCount + renameCount + 1) + ")";
- final String truncatedServiceName = MdnsUtils.truncateServiceName(mOriginalName,
+ final String suffix = " (" + (mServiceNameConflictCount + renameCount + 1) + ")";
+ final String truncatedServiceName = MdnsUtils.truncateServiceName(mOriginalServiceName,
MAX_LABEL_LENGTH - suffix.length());
return truncatedServiceName + suffix;
}
+ private String getUpdatedHostname(int renameCount) {
+ final String suffix = "-" + (mHostnameConflictCount + renameCount + 1);
+ final String truncatedHostname =
+ MdnsUtils.truncateServiceName(
+ mOriginalHostname, MAX_LABEL_LENGTH - suffix.length());
+ return truncatedHostname + suffix;
+ }
+
@NonNull
public NsdServiceInfo getServiceInfo() {
return mServiceInfo;
}
+
+ @NonNull
+ public MdnsAdvertisingOptions getAdvertisingOptions() {
+ return mAdvertisingOptions;
+ }
}
/**
@@ -621,14 +802,16 @@
public MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
@NonNull AdvertiserCallback cb, @NonNull SharedLog sharedLog,
- @NonNull MdnsFeatureFlags mDnsFeatureFlags) {
- this(looper, socketProvider, cb, new Dependencies(), sharedLog, mDnsFeatureFlags);
+ @NonNull MdnsFeatureFlags mDnsFeatureFlags, @NonNull Context context) {
+ this(looper, socketProvider, cb, new Dependencies(), sharedLog, mDnsFeatureFlags,
+ context);
}
@VisibleForTesting
MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
@NonNull AdvertiserCallback cb, @NonNull Dependencies deps,
- @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mDnsFeatureFlags) {
+ @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mDnsFeatureFlags,
+ @NonNull Context context) {
mLooper = looper;
mCb = cb;
mSocketProvider = socketProvider;
@@ -636,6 +819,31 @@
mDeviceHostName = deps.generateHostname();
mSharedLog = sharedLog;
mMdnsFeatureFlags = mDnsFeatureFlags;
+ final ConnectivityResources res = new ConnectivityResources(context);
+ mServiceTypeToOffloadPriority = parseOffloadPriorityList(
+ res.get().getStringArray(R.array.config_nsdOffloadServicesPriority), sharedLog);
+ }
+
+ private static Map<String, Integer> parseOffloadPriorityList(
+ @NonNull String[] resValues, SharedLog sharedLog) {
+ final Map<String, Integer> priorities = new ArrayMap<>(resValues.length);
+ for (String entry : resValues) {
+ final String[] priorityAndType = entry.split(":", 2);
+ if (priorityAndType.length != 2) {
+ sharedLog.wtf("Invalid config_nsdOffloadServicesPriority ignored: " + entry);
+ continue;
+ }
+
+ final int priority;
+ try {
+ priority = Integer.parseInt(priorityAndType[0]);
+ } catch (NumberFormatException e) {
+ sharedLog.wtf("Invalid priority in config_nsdOffloadServicesPriority: " + entry);
+ continue;
+ }
+ priorities.put(MdnsUtils.toDnsLowerCase(priorityAndType[1]), priority);
+ }
+ return priorities;
}
private void checkThread() {
@@ -650,9 +858,10 @@
* @param id A unique ID for the service.
* @param service The service info to advertise.
* @param advertisingOptions The advertising options.
+ * @param clientUid The UID who wants to advertise the service.
*/
public void addOrUpdateService(int id, NsdServiceInfo service,
- MdnsAdvertisingOptions advertisingOptions) {
+ MdnsAdvertisingOptions advertisingOptions, int clientUid) {
checkThread();
final Registration existingRegistration = mRegistrations.get(id);
final Network network = service.getNetwork();
@@ -664,7 +873,7 @@
mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
return;
}
- if (!(existingRegistration.matches(service))) {
+ if (!(existingRegistration.isSubtypeOnlyUpdate(service))) {
mSharedLog.e("Update request can only update subType, serviceInfo: " + service
+ ", existing serviceInfo: " + existingRegistration.getServiceInfo());
mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
@@ -684,7 +893,7 @@
}
mSharedLog.i("Adding service " + service + " with ID " + id + " and subtypes "
+ subtypes + " advertisingOptions " + advertisingOptions);
- registration = new Registration(service);
+ registration = new Registration(service, clientUid, advertisingOptions);
final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
if (network == null) {
// If registering on all networks, no advertiser must have conflicts
@@ -777,16 +986,17 @@
private OffloadServiceInfoWrapper createOffloadService(int serviceId,
@NonNull Registration registration, byte[] rawOffloadPacket) {
final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
+ final Integer mapPriority = mServiceTypeToOffloadPriority.get(
+ MdnsUtils.toDnsLowerCase(nsdServiceInfo.getServiceType()));
+ // Higher values of priority are less prioritized
+ final int priority = mapPriority == null ? Integer.MAX_VALUE : mapPriority;
final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
new OffloadServiceInfo.Key(nsdServiceInfo.getServiceName(),
nsdServiceInfo.getServiceType()),
new ArrayList<>(nsdServiceInfo.getSubtypes()),
String.join(".", mDeviceHostName),
rawOffloadPacket,
- // TODO: define overlayable resources in
- // ServiceConnectivityResources that set the priority based on
- // service type.
- 0 /* priority */,
+ priority,
// TODO: set the offloadType based on the callback timing.
OffloadEngine.OFFLOAD_TYPE_REPLY);
return new OffloadServiceInfoWrapper(serviceId, offloadServiceInfo);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
index e7a6ca7..a81d1e4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
@@ -16,6 +16,11 @@
package com.android.server.connectivity.mdns;
+import android.annotation.Nullable;
+
+import java.time.Duration;
+import java.util.Objects;
+
/**
* API configuration parameters for advertising the mDNS service.
*
@@ -27,13 +32,15 @@
private static MdnsAdvertisingOptions sDefaultOptions;
private final boolean mIsOnlyUpdate;
+ @Nullable
+ private final Duration mTtl;
/**
* Parcelable constructs for a {@link MdnsAdvertisingOptions}.
*/
- MdnsAdvertisingOptions(
- boolean isOnlyUpdate) {
+ MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl) {
this.mIsOnlyUpdate = isOnlyUpdate;
+ this.mTtl = ttl;
}
/**
@@ -60,9 +67,36 @@
return mIsOnlyUpdate;
}
+ /**
+ * Returns the TTL for all records in a service.
+ */
+ @Nullable
+ public Duration getTtl() {
+ return mTtl;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof MdnsAdvertisingOptions)) {
+ return false;
+ } else {
+ final MdnsAdvertisingOptions otherOptions = (MdnsAdvertisingOptions) other;
+ return mIsOnlyUpdate == otherOptions.mIsOnlyUpdate
+ && Objects.equals(mTtl, otherOptions.mTtl);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIsOnlyUpdate, mTtl);
+ }
+
@Override
public String toString() {
- return "MdnsAdvertisingOptions{" + "mIsOnlyUpdate=" + mIsOnlyUpdate + '}';
+ return "MdnsAdvertisingOptions{" + "mIsOnlyUpdate=" + mIsOnlyUpdate + ", mTtl=" + mTtl
+ + '}';
}
/**
@@ -70,6 +104,8 @@
*/
public static final class Builder {
private boolean mIsOnlyUpdate = false;
+ @Nullable
+ private Duration mTtl;
private Builder() {
}
@@ -83,10 +119,18 @@
}
/**
+ * Sets the TTL duration for all records of the service.
+ */
+ public Builder setTtl(@Nullable Duration ttl) {
+ this.mTtl = ttl;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsAdvertisingOptions} with the arguments supplied to this builder.
*/
public MdnsAdvertisingOptions build() {
- return new MdnsAdvertisingOptions(mIsOnlyUpdate);
+ return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl);
}
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 766f999..0ab7a76 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -16,26 +16,29 @@
package com.android.server.connectivity.mdns;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
-
import android.Manifest.permission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.Looper;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
+import androidx.annotation.GuardedBy;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.Executor;
/**
* This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
@@ -50,11 +53,13 @@
@NonNull private final SharedLog sharedLog;
@NonNull private final PerSocketServiceTypeClients perSocketServiceTypeClients;
- @NonNull private final Handler handler;
- @Nullable private final HandlerThread handlerThread;
- @NonNull private final MdnsServiceCache serviceCache;
+ @NonNull private final DiscoveryExecutor discoveryExecutor;
@NonNull private final MdnsFeatureFlags mdnsFeatureFlags;
+ // Only accessed on the handler thread, initialized before first use
+ @Nullable
+ private MdnsServiceCache serviceCache;
+
private static class PerSocketServiceTypeClients {
private final ArrayMap<Pair<String, SocketKey>, MdnsServiceTypeClient> clients =
new ArrayMap<>();
@@ -125,33 +130,82 @@
this.sharedLog = sharedLog;
this.perSocketServiceTypeClients = new PerSocketServiceTypeClients();
this.mdnsFeatureFlags = mdnsFeatureFlags;
- if (socketClient.getLooper() != null) {
- this.handlerThread = null;
- this.handler = new Handler(socketClient.getLooper());
- this.serviceCache = new MdnsServiceCache(socketClient.getLooper(), mdnsFeatureFlags);
- } else {
- this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName());
- this.handlerThread.start();
- this.handler = new Handler(handlerThread.getLooper());
- this.serviceCache = new MdnsServiceCache(handlerThread.getLooper(), mdnsFeatureFlags);
- }
+ this.discoveryExecutor = new DiscoveryExecutor(socketClient.getLooper());
}
- private void checkAndRunOnHandlerThread(@NonNull Runnable function) {
- if (this.handlerThread == null) {
- function.run();
- } else {
+ private static class DiscoveryExecutor implements Executor {
+ private final HandlerThread handlerThread;
+
+ @GuardedBy("pendingTasks")
+ @Nullable private Handler handler;
+ @GuardedBy("pendingTasks")
+ @NonNull private final ArrayList<Runnable> pendingTasks = new ArrayList<>();
+
+ DiscoveryExecutor(@Nullable Looper defaultLooper) {
+ if (defaultLooper != null) {
+ this.handlerThread = null;
+ synchronized (pendingTasks) {
+ this.handler = new Handler(defaultLooper);
+ }
+ } else {
+ this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName()) {
+ @Override
+ protected void onLooperPrepared() {
+ synchronized (pendingTasks) {
+ handler = new Handler(getLooper());
+ for (Runnable pendingTask : pendingTasks) {
+ handler.post(pendingTask);
+ }
+ pendingTasks.clear();
+ }
+ }
+ };
+ this.handlerThread.start();
+ }
+ }
+
+ public void checkAndRunOnHandlerThread(@NonNull Runnable function) {
+ if (this.handlerThread == null) {
+ // Callers are expected to already be running on the handler when a defaultLooper
+ // was provided
+ function.run();
+ } else {
+ execute(function);
+ }
+ }
+
+ @Override
+ public void execute(Runnable function) {
+ final Handler handler;
+ synchronized (pendingTasks) {
+ if (this.handler == null) {
+ pendingTasks.add(function);
+ return;
+ } else {
+ handler = this.handler;
+ }
+ }
handler.post(function);
}
+
+ void shutDown() {
+ if (this.handlerThread != null) {
+ this.handlerThread.quitSafely();
+ }
+ }
+
+ void ensureRunningOnHandlerThread() {
+ synchronized (pendingTasks) {
+ MdnsUtils.ensureRunningOnHandlerThread(handler);
+ }
+ }
}
/**
* Do the cleanup of the MdnsDiscoveryManager
*/
public void shutDown() {
- if (this.handlerThread != null) {
- this.handlerThread.quitSafely();
- }
+ discoveryExecutor.shutDown();
}
/**
@@ -169,7 +223,7 @@
@NonNull MdnsServiceBrowserListener listener,
@NonNull MdnsSearchOptions searchOptions) {
sharedLog.i("Registering listener for serviceType: " + serviceType);
- checkAndRunOnHandlerThread(() ->
+ discoveryExecutor.checkAndRunOnHandlerThread(() ->
handleRegisterListener(serviceType, listener, searchOptions));
}
@@ -187,11 +241,30 @@
}
}
// Request the network for discovery.
+ // This requests sockets on all networks even if the searchOptions have a given interface
+ // index (with getNetwork==null, for local interfaces), and only uses matching interfaces
+ // in that case. While this is a simple solution to only use matching sockets, a better
+ // practice would be to only request the correct socket for discovery.
+ // TODO: avoid requesting extra sockets after migrating P2P and tethering networks to local
+ // NetworkAgents.
socketClient.notifyNetworkRequested(listener, searchOptions.getNetwork(),
new MdnsSocketClientBase.SocketCreationCallback() {
@Override
public void onSocketCreated(@NonNull SocketKey socketKey) {
- ensureRunningOnHandlerThread(handler);
+ discoveryExecutor.ensureRunningOnHandlerThread();
+ final int searchInterfaceIndex = searchOptions.getInterfaceIndex();
+ if (searchOptions.getNetwork() == null
+ && searchInterfaceIndex > 0
+ // The interface index in options should only match interfaces that
+ // do not have any Network; a matching Network should be provided
+ // otherwise.
+ && (socketKey.getNetwork() != null
+ || socketKey.getInterfaceIndex() != searchInterfaceIndex)) {
+ sharedLog.i("Skipping " + socketKey + " as ifIndex "
+ + searchInterfaceIndex + " was requested.");
+ return;
+ }
+
// All listeners of the same service types shares the same
// MdnsServiceTypeClient.
MdnsServiceTypeClient serviceTypeClient =
@@ -206,7 +279,7 @@
@Override
public void onSocketDestroyed(@NonNull SocketKey socketKey) {
- ensureRunningOnHandlerThread(handler);
+ discoveryExecutor.ensureRunningOnHandlerThread();
final MdnsServiceTypeClient serviceTypeClient =
perSocketServiceTypeClients.get(serviceType, socketKey);
if (serviceTypeClient == null) return;
@@ -229,7 +302,8 @@
public void unregisterListener(
@NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
sharedLog.i("Unregistering listener for serviceType:" + serviceType);
- checkAndRunOnHandlerThread(() -> handleUnregisterListener(serviceType, listener));
+ discoveryExecutor.checkAndRunOnHandlerThread(() ->
+ handleUnregisterListener(serviceType, listener));
}
private void handleUnregisterListener(
@@ -260,7 +334,7 @@
@Override
public void onResponseReceived(@NonNull MdnsPacket packet, @NonNull SocketKey socketKey) {
- checkAndRunOnHandlerThread(() ->
+ discoveryExecutor.checkAndRunOnHandlerThread(() ->
handleOnResponseReceived(packet, socketKey));
}
@@ -282,7 +356,7 @@
@Override
public void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
@NonNull SocketKey socketKey) {
- checkAndRunOnHandlerThread(() ->
+ discoveryExecutor.checkAndRunOnHandlerThread(() ->
handleOnFailedToParseMdnsResponse(receivedPacketNumber, errorCode, socketKey));
}
@@ -296,12 +370,31 @@
@VisibleForTesting
MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
@NonNull SocketKey socketKey) {
+ discoveryExecutor.ensureRunningOnHandlerThread();
sharedLog.log("createServiceTypeClient for type:" + serviceType + " " + socketKey);
final String tag = serviceType + "-" + socketKey.getNetwork()
+ "/" + socketKey.getInterfaceIndex();
+ final Looper looper = Looper.myLooper();
+ if (serviceCache == null) {
+ serviceCache = new MdnsServiceCache(looper, mdnsFeatureFlags);
+ }
return new MdnsServiceTypeClient(
serviceType, socketClient,
executorProvider.newServiceTypeClientSchedulerExecutor(), socketKey,
- sharedLog.forSubComponent(tag), handler.getLooper(), serviceCache);
+ sharedLog.forSubComponent(tag), looper, serviceCache, mdnsFeatureFlags);
+ }
+
+ /**
+ * Dump DiscoveryManager state.
+ */
+ public void dump(PrintWriter pw) {
+ discoveryExecutor.checkAndRunOnHandlerThread(() -> {
+ pw.println();
+ // Dump ServiceTypeClients
+ for (MdnsServiceTypeClient serviceTypeClient
+ : perSocketServiceTypeClients.getAllMdnsServiceTypeClient()) {
+ serviceTypeClient.dump(pw);
+ }
+ });
}
}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index 1ad47a3..f4a08ba 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;
*/
@@ -46,6 +49,24 @@
*/
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";
+
+ /**
+ * A feature flag to control whether the aggressive query mode should be enabled.
+ */
+ public static final String NSD_AGGRESSIVE_QUERY_MODE = "nsd_aggressive_query_mode";
+
+ /**
+ * A feature flag to control whether the query with known-answer should be enabled.
+ */
+ public static final String NSD_QUERY_WITH_KNOWN_ANSWER = "nsd_query_with_known_answer";
+
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
@@ -61,6 +82,65 @@
// Flag for known-answer suppression
public final boolean mIsKnownAnswerSuppressionEnabled;
+ // Flag to enable replying unicast to queries requesting unicast replies
+ public final boolean mIsUnicastReplyEnabled;
+
+ // Flag for aggressive query mode
+ public final boolean mIsAggressiveQueryModeEnabled;
+
+ // Flag for query with known-answer
+ public final boolean mIsQueryWithKnownAnswerEnabled;
+
+ @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);
+ }
+
+ /**
+ * Indicates whether {@link #NSD_AGGRESSIVE_QUERY_MODE} is enabled, including for testing.
+ */
+ public boolean isAggressiveQueryModeEnabled() {
+ return mIsAggressiveQueryModeEnabled || isForceEnabledForTest(NSD_AGGRESSIVE_QUERY_MODE);
+ }
+
+ /**
+ * Indicates whether {@link #NSD_KNOWN_ANSWER_SUPPRESSION} is enabled, including for testing.
+ */
+ public boolean isKnownAnswerSuppressionEnabled() {
+ return mIsKnownAnswerSuppressionEnabled
+ || isForceEnabledForTest(NSD_KNOWN_ANSWER_SUPPRESSION);
+ }
+
+ /**
+ * Indicates whether {@link #NSD_QUERY_WITH_KNOWN_ANSWER} is enabled, including for testing.
+ */
+ public boolean isQueryWithKnownAnswerEnabled() {
+ return mIsQueryWithKnownAnswerEnabled
+ || isForceEnabledForTest(NSD_QUERY_WITH_KNOWN_ANSWER);
+ }
+
/**
* The constructor for {@link MdnsFeatureFlags}.
*/
@@ -68,12 +148,20 @@
boolean includeInetAddressRecordsInProbing,
boolean isExpiredServicesRemovalEnabled,
boolean isLabelCountLimitEnabled,
- boolean isKnownAnswerSuppressionEnabled) {
+ boolean isKnownAnswerSuppressionEnabled,
+ boolean isUnicastReplyEnabled,
+ boolean isAggressiveQueryModeEnabled,
+ boolean isQueryWithKnownAnswerEnabled,
+ @Nullable FlagOverrideProvider overrideProvider) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
mIsExpiredServicesRemovalEnabled = isExpiredServicesRemovalEnabled;
mIsLabelCountLimitEnabled = isLabelCountLimitEnabled;
mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
+ mIsUnicastReplyEnabled = isUnicastReplyEnabled;
+ mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
+ mIsQueryWithKnownAnswerEnabled = isQueryWithKnownAnswerEnabled;
+ mOverrideProvider = overrideProvider;
}
@@ -90,6 +178,10 @@
private boolean mIsExpiredServicesRemovalEnabled;
private boolean mIsLabelCountLimitEnabled;
private boolean mIsKnownAnswerSuppressionEnabled;
+ private boolean mIsUnicastReplyEnabled;
+ private boolean mIsAggressiveQueryModeEnabled;
+ private boolean mIsQueryWithKnownAnswerEnabled;
+ private FlagOverrideProvider mOverrideProvider;
/**
* The constructor for {@link Builder}.
@@ -100,6 +192,10 @@
mIsExpiredServicesRemovalEnabled = false;
mIsLabelCountLimitEnabled = true; // Default enabled.
mIsKnownAnswerSuppressionEnabled = false;
+ mIsUnicastReplyEnabled = true;
+ mIsAggressiveQueryModeEnabled = false;
+ mIsQueryWithKnownAnswerEnabled = false;
+ mOverrideProvider = null;
}
/**
@@ -154,6 +250,47 @@
}
/**
+ * 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;
+ }
+
+ /**
+ * Set whether the aggressive query mode is enabled.
+ *
+ * @see #NSD_AGGRESSIVE_QUERY_MODE
+ */
+ public Builder setIsAggressiveQueryModeEnabled(boolean isAggressiveQueryModeEnabled) {
+ mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
+ return this;
+ }
+
+ /**
+ * Set whether the query with known-answer is enabled.
+ *
+ * @see #NSD_QUERY_WITH_KNOWN_ANSWER
+ */
+ public Builder setIsQueryWithKnownAnswerEnabled(boolean isQueryWithKnownAnswerEnabled) {
+ mIsQueryWithKnownAnswerEnabled = isQueryWithKnownAnswerEnabled;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
*/
public MdnsFeatureFlags build() {
@@ -161,7 +298,11 @@
mIncludeInetAddressRecordsInProbing,
mIsExpiredServicesRemovalEnabled,
mIsLabelCountLimitEnabled,
- mIsKnownAnswerSuppressionEnabled);
+ mIsKnownAnswerSuppressionEnabled,
+ mIsUnicastReplyEnabled,
+ mIsAggressiveQueryModeEnabled,
+ mIsQueryWithKnownAnswerEnabled,
+ 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..0b2003f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -36,7 +36,9 @@
import java.io.IOException;
import java.net.InetSocketAddress;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -44,6 +46,9 @@
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class MdnsInterfaceAdvertiser implements MulticastPacketReader.PacketHandler {
+ public static final int CONFLICT_SERVICE = 1 << 0;
+ public static final int CONFLICT_HOST = 1 << 1;
+
private static final boolean DBG = MdnsAdvertiser.DBG;
@VisibleForTesting
public static final long EXIT_ANNOUNCEMENT_DELAY_MS = 100L;
@@ -85,18 +90,26 @@
/**
* Called by the advertiser when a conflict was found, during or after probing.
*
- * If a conflict is found during probing, the {@link #renameServiceForConflict} must be
+ * <p>If a conflict is found during probing, the {@link #renameServiceForConflict} must be
* called to restart probing and attempt registration with a different name.
+ *
+ * <p>{@code conflictType} is a bitmap telling which part of the service is conflicting. See
+ * {@link MdnsInterfaceAdvertiser#CONFLICT_SERVICE} and {@link
+ * MdnsInterfaceAdvertiser#CONFLICT_HOST}.
*/
- void onServiceConflict(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId);
+ void onServiceConflict(
+ @NonNull MdnsInterfaceAdvertiser advertiser, int serviceId, int conflictType);
/**
- * Called by the advertiser when it destroyed itself.
+ * Called when all services on this interface advertiser has already been removed and exit
+ * announcements have been sent.
*
- * This can happen after a call to {@link #destroyNow()}, or after all services were
- * unregistered and the advertiser finished sending exit announcements.
+ * <p>It's guaranteed that there are no service registrations in the
+ * MdnsInterfaceAdvertiser when this callback is invoked.
+ *
+ * <p>This is typically listened by the {@link MdnsAdvertiser} to release the resources
*/
- void onDestroyed(@NonNull MdnsInterfaceSocket socket);
+ void onAllServicesRemoved(@NonNull MdnsInterfaceSocket socket);
}
/**
@@ -122,6 +135,15 @@
mAnnouncer.startSending(info.getServiceId(), announcementInfo,
0L /* initialDelayMs */);
+
+ // Re-announce the services which have the same custom hostname.
+ final String hostname = mRecordRepository.getHostnameForServiceId(info.getServiceId());
+ if (hostname != null) {
+ final List<MdnsAnnouncer.AnnouncementInfo> announcementInfos =
+ new ArrayList<>(mRecordRepository.restartAnnouncingForHostname(hostname));
+ announcementInfos.removeIf((i) -> i.getServiceId() == info.getServiceId());
+ reannounceServices(announcementInfos);
+ }
}
}
@@ -138,10 +160,11 @@
public void onFinished(@NonNull BaseAnnouncementInfo info) {
if (info instanceof MdnsAnnouncer.ExitAnnouncementInfo) {
mRecordRepository.removeService(info.getServiceId());
-
- if (mRecordRepository.getServicesCount() == 0) {
- destroyNow();
- }
+ mCbHandler.post(() -> {
+ if (mRecordRepository.getServicesCount() == 0) {
+ mCb.onAllServicesRemoved(mSocket);
+ }
+ });
}
}
}
@@ -162,10 +185,11 @@
@NonNull
public MdnsReplySender makeReplySender(@NonNull String interfaceTag, @NonNull Looper looper,
@NonNull MdnsInterfaceSocket socket, @NonNull byte[] packetCreationBuffer,
- @NonNull SharedLog sharedLog) {
+ @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
return new MdnsReplySender(looper, socket, packetCreationBuffer,
sharedLog.forSubComponent(
- MdnsReplySender.class.getSimpleName() + "/" + interfaceTag), DBG);
+ MdnsReplySender.class.getSimpleName() + "/" + interfaceTag), DBG,
+ mdnsFeatureFlags);
}
/** @see MdnsAnnouncer */
@@ -208,7 +232,7 @@
mCb = cb;
mCbHandler = new Handler(looper);
mReplySender = deps.makeReplySender(sharedLog.getTag(), looper, socket,
- packetCreationBuffer, sharedLog);
+ packetCreationBuffer, sharedLog, mdnsFeatureFlags);
mPacketCreationBuffer = packetCreationBuffer;
mAnnouncer = deps.makeMdnsAnnouncer(sharedLog.getTag(), looper, mReplySender,
mAnnouncingCallback, sharedLog);
@@ -222,8 +246,7 @@
* Start the advertiser.
*
* The advertiser will stop itself when all services are removed and exit announcements sent,
- * notifying via {@link Callback#onDestroyed}. This can also be triggered manually via
- * {@link #destroyNow()}.
+ * notifying via {@link Callback#onAllServicesRemoved}.
*/
public void start() {
mSocket.addPacketHandler(this);
@@ -246,8 +269,10 @@
*
* @throws NameConflictException There is already a service being advertised with that name.
*/
- public void addService(int id, NsdServiceInfo service) throws NameConflictException {
- final int replacedExitingService = mRecordRepository.addService(id, service);
+ public void addService(int id, NsdServiceInfo service,
+ @NonNull MdnsAdvertisingOptions advertisingOptions) throws NameConflictException {
+ final int replacedExitingService =
+ mRecordRepository.addService(id, service, advertisingOptions.getTtl());
// Cancel announcements for the existing service. This only happens for exiting services
// (so cancelling exiting announcements), as per RecordRepository.addService.
if (replacedExitingService >= 0) {
@@ -267,10 +292,11 @@
if (!mRecordRepository.hasActiveService(id)) return;
mProber.stop(id);
mAnnouncer.stop(id);
+ final String hostname = mRecordRepository.getHostnameForServiceId(id);
final MdnsAnnouncer.ExitAnnouncementInfo exitInfo = mRecordRepository.exitService(id);
if (exitInfo != null) {
- // This effectively schedules destroyNow(), as it is to be called when the exit
- // announcement finishes if there is no service left.
+ // This effectively schedules onAllServicesRemoved(), as it is to be called when the
+ // exit announcement finishes if there is no service left.
// A non-zero exit announcement delay follows legacy mdnsresponder behavior, and is
// also useful to ensure that when a host receives the exit announcement, the service
// has been unregistered on all interfaces; so an announcement sent from interface A
@@ -280,9 +306,22 @@
} else {
// No exit announcement necessary: remove the service immediately.
mRecordRepository.removeService(id);
- if (mRecordRepository.getServicesCount() == 0) {
- destroyNow();
- }
+ mCbHandler.post(() -> {
+ if (mRecordRepository.getServicesCount() == 0) {
+ mCb.onAllServicesRemoved(mSocket);
+ }
+ });
+ }
+ // Re-probe/re-announce the services which have the same custom hostname. These services
+ // were probed/announced using host addresses which were just removed so they should be
+ // re-probed/re-announced without those addresses.
+ if (hostname != null) {
+ final List<MdnsProber.ProbingInfo> probingInfos =
+ mRecordRepository.restartProbingForHostname(hostname);
+ reprobeServices(probingInfos);
+ final List<MdnsAnnouncer.AnnouncementInfo> announcementInfos =
+ mRecordRepository.restartAnnouncingForHostname(hostname);
+ reannounceServices(announcementInfos);
}
}
@@ -316,7 +355,8 @@
/**
* Destroy the advertiser immediately, not sending any exit announcement.
*
- * <p>Useful when the underlying network went away. This will trigger an onDestroyed callback.
+ * <p>This is typically called when all services on the interface are removed or when the
+ * underlying network went away.
*/
public void destroyNow() {
for (int serviceId : mRecordRepository.clearServices()) {
@@ -325,7 +365,6 @@
}
mReplySender.cancelAll();
mSocket.removePacketHandler(this);
- mCbHandler.post(() -> mCb.onDestroyed(mSocket));
}
/**
@@ -335,6 +374,7 @@
final MdnsProber.ProbingInfo probingInfo = mRecordRepository.setServiceProbing(serviceId);
if (probingInfo == null) return false;
+ mAnnouncer.stop(serviceId);
mProber.restartForConflict(probingInfo);
return true;
}
@@ -373,23 +413,33 @@
}
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)) {
- mCbHandler.post(() -> mCb.onServiceConflict(this, conflictServiceId));
+ Map<Integer, Integer> conflictingServices =
+ mRecordRepository.getConflictingServices(packet);
+
+ for (Map.Entry<Integer, Integer> entry : conflictingServices.entrySet()) {
+ int serviceId = entry.getKey();
+ int conflictType = entry.getValue();
+ mCbHandler.post(
+ () -> {
+ mCb.onServiceConflict(this, serviceId, conflictType);
+ });
}
// Even in case of conflict, add replies for other services. But in general conflicts would
// 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);
@@ -417,4 +467,19 @@
return new byte[0];
}
}
+
+ private void reprobeServices(List<MdnsProber.ProbingInfo> probingInfos) {
+ for (MdnsProber.ProbingInfo probingInfo : probingInfos) {
+ mProber.stop(probingInfo.getServiceId());
+ mProber.startProbing(probingInfo);
+ }
+ }
+
+ private void reannounceServices(List<MdnsAnnouncer.AnnouncementInfo> announcementInfos) {
+ for (MdnsAnnouncer.AnnouncementInfo announcementInfo : announcementInfos) {
+ mAnnouncer.stop(announcementInfo.getServiceId());
+ mAnnouncer.startSending(
+ announcementInfo.getServiceId(), announcementInfo, 0 /* initialDelayMs */);
+ }
+ }
}
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..fcfb15f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -27,6 +27,7 @@
import android.os.Handler;
import android.os.Looper;
import android.util.ArrayMap;
+import android.util.Log;
import com.android.net.module.util.SharedLog;
@@ -96,7 +97,8 @@
@Override
public void onInterfaceDestroyed(@NonNull SocketKey socketKey,
@NonNull MdnsInterfaceSocket socket) {
- notifySocketDestroyed(socketKey);
+ mActiveSockets.remove(socketKey);
+ mSocketCreationCallback.onSocketDestroyed(socketKey);
maybeCleanupPacketHandler(socketKey);
}
@@ -212,24 +214,30 @@
return true;
}
- private void sendMdnsPacket(@NonNull DatagramPacket packet, @NonNull SocketKey targetSocketKey,
- boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+ private void sendMdnsPackets(@NonNull List<DatagramPacket> packets,
+ @NonNull SocketKey targetSocketKey, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
final MdnsInterfaceSocket socket = getTargetSocket(targetSocketKey);
if (socket == null) {
mSharedLog.e("No socket matches targetSocketKey=" + targetSocketKey);
return;
}
+ if (packets.isEmpty()) {
+ Log.wtf(TAG, "No mDns packets to send");
+ return;
+ }
- final boolean isIpv6 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
- instanceof Inet6Address;
- final boolean isIpv4 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
- instanceof Inet4Address;
+ final boolean isIpv6 = ((InetSocketAddress) packets.get(0).getSocketAddress())
+ .getAddress() instanceof Inet6Address;
+ final boolean isIpv4 = ((InetSocketAddress) packets.get(0).getSocketAddress())
+ .getAddress() instanceof Inet4Address;
final boolean shouldQueryIpv6 = !onlyUseIpv6OnIpv6OnlyNetworks || !socket.hasJoinedIpv4();
// Check ip capability and network before sending packet
if ((isIpv6 && socket.hasJoinedIpv6() && shouldQueryIpv6)
|| (isIpv4 && socket.hasJoinedIpv4())) {
try {
- socket.send(packet);
+ for (DatagramPacket packet : packets) {
+ socket.send(packet);
+ }
} catch (IOException e) {
mSharedLog.e("Failed to send a mDNS packet.", e);
}
@@ -258,34 +266,34 @@
}
/**
- * Send a mDNS request packet via given socket key that asks for multicast response.
+ * Send mDNS request packets via given socket key that asks for multicast response.
*/
- public void sendPacketRequestingMulticastResponse(@NonNull DatagramPacket packet,
+ public void sendPacketRequestingMulticastResponse(@NonNull List<DatagramPacket> packets,
@NonNull SocketKey socketKey, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
- mHandler.post(() -> sendMdnsPacket(packet, socketKey, onlyUseIpv6OnIpv6OnlyNetworks));
+ mHandler.post(() -> sendMdnsPackets(packets, socketKey, onlyUseIpv6OnIpv6OnlyNetworks));
}
@Override
public void sendPacketRequestingMulticastResponse(
- @NonNull DatagramPacket packet, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+ @NonNull List<DatagramPacket> packets, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
throw new UnsupportedOperationException("This socket client need to specify the socket to"
+ "send packet");
}
/**
- * Send a mDNS request packet via given socket key that asks for unicast response.
+ * Send mDNS request packets via given socket key that asks for unicast response.
*
* <p>The socket client may use a null network to identify some or all interfaces, in which case
* passing null sends the packet to these.
*/
- public void sendPacketRequestingUnicastResponse(@NonNull DatagramPacket packet,
+ public void sendPacketRequestingUnicastResponse(@NonNull List<DatagramPacket> packets,
@NonNull SocketKey socketKey, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
- mHandler.post(() -> sendMdnsPacket(packet, socketKey, onlyUseIpv6OnIpv6OnlyNetworks));
+ mHandler.post(() -> sendMdnsPackets(packets, socketKey, onlyUseIpv6OnIpv6OnlyNetworks));
}
@Override
public void sendPacketRequestingUnicastResponse(
- @NonNull DatagramPacket packet, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+ @NonNull List<DatagramPacket> packets, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
throw new UnsupportedOperationException("This socket client need to specify the socket to"
+ "send packet");
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
index 1fabd49..83ecabc 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
@@ -42,7 +42,7 @@
@NonNull
public final List<MdnsRecord> additionalRecords;
- MdnsPacket(int flags,
+ public MdnsPacket(int flags,
@NonNull List<MdnsRecord> questions,
@NonNull List<MdnsRecord> answers,
@NonNull List<MdnsRecord> authorityRecords,
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..1f9f42b 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -23,7 +23,8 @@
import android.os.SystemClock;
import android.text.TextUtils;
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
+
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
@@ -176,6 +177,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
@@ -221,7 +232,7 @@
* @param writer The writer to use.
* @param now The current system time. This is used when writing the updated TTL.
*/
- @VisibleForTesting
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public final void write(MdnsPacketWriter writer, long now) throws IOException {
writeHeaderFields(writer);
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 6b6632c..073e465 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -19,6 +19,8 @@
import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
+import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_HOST;
+import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -28,6 +30,8 @@
import android.os.Build;
import android.os.Looper;
import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.SparseArray;
@@ -41,6 +45,7 @@
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -54,6 +59,8 @@
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
/**
* A repository of records advertised through {@link MdnsInterfaceAdvertiser}.
@@ -69,9 +76,9 @@
// TTL for 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)
- private static final long NAME_RECORDS_TTL_MILLIS = TimeUnit.SECONDS.toMillis(120);
+ private static final long DEFAULT_NAME_RECORDS_TTL_MILLIS = TimeUnit.SECONDS.toMillis(120);
// TTL for other records
- private static final long NON_NAME_RECORDS_TTL_MILLIS = TimeUnit.MINUTES.toMillis(75);
+ private static final long DEFAULT_NON_NAME_RECORDS_TTL_MILLIS = TimeUnit.MINUTES.toMillis(75);
// Top-level domain for link-local queries, as per RFC6762 3.
private static final String LOCAL_TLD = "local";
@@ -158,11 +165,13 @@
public final List<RecordInfo<?>> allRecords;
@NonNull
public final List<RecordInfo<MdnsPointerRecord>> ptrRecords;
- @NonNull
+ @Nullable
public final RecordInfo<MdnsServiceRecord> srvRecord;
- @NonNull
+ @Nullable
public final RecordInfo<MdnsTextRecord> txtRecord;
@NonNull
+ public final List<RecordInfo<MdnsInetAddressRecord>> addressRecords;
+ @NonNull
public final NsdServiceInfo serviceInfo;
/**
@@ -185,6 +194,9 @@
*/
private boolean isProbing;
+ @Nullable
+ private Duration ttl;
+
/**
* Create a ServiceRegistration with only update the subType.
*/
@@ -192,75 +204,122 @@
NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
newServiceInfo.setSubtypes(newSubtypes);
return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
- repliedServiceCount, sentPacketCount, exiting, isProbing);
+ repliedServiceCount, sentPacketCount, exiting, isProbing, ttl);
}
/**
* Create a ServiceRegistration for dns-sd service registration (RFC6763).
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
- int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing) {
+ int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing,
+ @Nullable Duration ttl) {
this.serviceInfo = serviceInfo;
- final String[] serviceType = splitServiceType(serviceInfo);
- final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
+ final long nonNameRecordsTtlMillis;
+ final long nameRecordsTtlMillis;
- // Service PTR records
- ptrRecords = new ArrayList<>(serviceInfo.getSubtypes().size() + 1);
- ptrRecords.add(new RecordInfo<>(
- serviceInfo,
- new MdnsPointerRecord(
- serviceType,
- 0L /* receiptTimeMillis */,
- false /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
- serviceName),
- true /* sharedName */));
- for (String subtype : serviceInfo.getSubtypes()) {
- ptrRecords.add(new RecordInfo<>(
- serviceInfo,
- new MdnsPointerRecord(
- MdnsUtils.constructFullSubtype(serviceType, subtype),
- 0L /* receiptTimeMillis */,
- false /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
- serviceName),
- true /* sharedName */));
+ // When custom TTL is specified, all records of the service will use the custom TTL.
+ // This is typically useful for SRP (Service Registration Protocol:
+ // https://datatracker.ietf.org/doc/html/draft-ietf-dnssd-srp-24) Advertising Proxy
+ // where all records in a single SRP are required the same TTL.
+ if (ttl != null) {
+ nonNameRecordsTtlMillis = ttl.toMillis();
+ nameRecordsTtlMillis = ttl.toMillis();
+ } else {
+ nonNameRecordsTtlMillis = DEFAULT_NON_NAME_RECORDS_TTL_MILLIS;
+ nameRecordsTtlMillis = DEFAULT_NAME_RECORDS_TTL_MILLIS;
}
- srvRecord = new RecordInfo<>(
- serviceInfo,
- new MdnsServiceRecord(serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- NAME_RECORDS_TTL_MILLIS, 0 /* servicePriority */, 0 /* serviceWeight */,
- serviceInfo.getPort(),
- deviceHostname),
- false /* sharedName */);
-
- txtRecord = new RecordInfo<>(
- serviceInfo,
- new MdnsTextRecord(serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */, // Service name is verified unique after probing
- NON_NAME_RECORDS_TTL_MILLIS,
- attrsToTextEntries(serviceInfo.getAttributes())),
- false /* sharedName */);
-
+ final boolean hasService = !TextUtils.isEmpty(serviceInfo.getServiceType());
+ final boolean hasCustomHost = !TextUtils.isEmpty(serviceInfo.getHostname());
+ final String[] hostname =
+ hasCustomHost
+ ? new String[] {serviceInfo.getHostname(), LOCAL_TLD}
+ : deviceHostname;
final ArrayList<RecordInfo<?>> allRecords = new ArrayList<>(5);
- allRecords.addAll(ptrRecords);
- allRecords.add(srvRecord);
- allRecords.add(txtRecord);
- // Service type enumeration record (RFC6763 9.)
- allRecords.add(new RecordInfo<>(
- serviceInfo,
- new MdnsPointerRecord(
- DNS_SD_SERVICE_TYPE,
- 0L /* receiptTimeMillis */,
- false /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
- serviceType),
- true /* sharedName */));
+
+ if (hasService) {
+ final String[] serviceType = splitServiceType(serviceInfo);
+ final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
+ // Service PTR records
+ ptrRecords = new ArrayList<>(serviceInfo.getSubtypes().size() + 1);
+ ptrRecords.add(new RecordInfo<>(
+ serviceInfo,
+ new MdnsPointerRecord(
+ serviceType,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ nonNameRecordsTtlMillis,
+ serviceName),
+ true /* sharedName */));
+ for (String subtype : serviceInfo.getSubtypes()) {
+ ptrRecords.add(new RecordInfo<>(
+ serviceInfo,
+ new MdnsPointerRecord(
+ MdnsUtils.constructFullSubtype(serviceType, subtype),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ nonNameRecordsTtlMillis,
+ serviceName),
+ true /* sharedName */));
+ }
+
+ srvRecord = new RecordInfo<>(
+ serviceInfo,
+ new MdnsServiceRecord(serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ nameRecordsTtlMillis,
+ 0 /* servicePriority */, 0 /* serviceWeight */,
+ serviceInfo.getPort(),
+ hostname),
+ false /* sharedName */);
+
+ txtRecord = new RecordInfo<>(
+ serviceInfo,
+ new MdnsTextRecord(serviceName,
+ 0L /* receiptTimeMillis */,
+ // Service name is verified unique after probing
+ true /* cacheFlush */,
+ nonNameRecordsTtlMillis,
+ attrsToTextEntries(serviceInfo.getAttributes())),
+ false /* sharedName */);
+
+ allRecords.addAll(ptrRecords);
+ allRecords.add(srvRecord);
+ allRecords.add(txtRecord);
+ // Service type enumeration record (RFC6763 9.)
+ allRecords.add(new RecordInfo<>(
+ serviceInfo,
+ new MdnsPointerRecord(
+ DNS_SD_SERVICE_TYPE,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ nonNameRecordsTtlMillis,
+ serviceType),
+ true /* sharedName */));
+ } else {
+ ptrRecords = Collections.emptyList();
+ srvRecord = null;
+ txtRecord = null;
+ }
+
+ if (hasCustomHost) {
+ addressRecords = new ArrayList<>(serviceInfo.getHostAddresses().size());
+ for (InetAddress address : serviceInfo.getHostAddresses()) {
+ addressRecords.add(new RecordInfo<>(
+ serviceInfo,
+ new MdnsInetAddressRecord(hostname,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ nameRecordsTtlMillis,
+ address),
+ false /* sharedName */));
+ }
+ allRecords.addAll(addressRecords);
+ } else {
+ addressRecords = Collections.emptyList();
+ }
this.allRecords = Collections.unmodifiableList(allRecords);
this.repliedServiceCount = repliedServiceCount;
@@ -276,9 +335,9 @@
* @param serviceInfo Service to advertise
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
- int repliedServiceCount, int sentPacketCount) {
+ int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl) {
this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
- false /* exiting */, true /* isProbing */);
+ false /* exiting */, true /* isProbing */, ttl);
}
void setProbing(boolean probing) {
@@ -300,7 +359,7 @@
revDnsAddr,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- NAME_RECORDS_TTL_MILLIS,
+ DEFAULT_NAME_RECORDS_TTL_MILLIS,
mDeviceHostname),
false /* sharedName */));
@@ -310,7 +369,7 @@
mDeviceHostname,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- NAME_RECORDS_TTL_MILLIS,
+ DEFAULT_NAME_RECORDS_TTL_MILLIS,
addr.getAddress()),
false /* sharedName */));
}
@@ -339,17 +398,20 @@
* This may remove/replace any existing service that used the name added but is exiting.
* @param serviceId A unique service ID.
* @param serviceInfo Service info to add.
+ * @param ttl the TTL duration for all records of {@code serviceInfo} or {@code null}
* @return If the added service replaced another with a matching name (which was exiting), the
* ID of the replaced service.
* @throws NameConflictException There is already a (non-exiting) service using the name.
*/
- public int addService(int serviceId, NsdServiceInfo serviceInfo) throws NameConflictException {
+ public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable Duration ttl)
+ throws NameConflictException {
if (mServices.contains(serviceId)) {
throw new IllegalArgumentException(
"Service ID must not be reused across registrations: " + serviceId);
}
- final int existing = getServiceByName(serviceInfo.getServiceName());
+ final int existing =
+ getServiceByNameAndType(serviceInfo.getServiceName(), serviceInfo.getServiceType());
// It's OK to re-add a service that is exiting
if (existing >= 0 && !mServices.get(existing).exiting) {
throw new NameConflictException(existing);
@@ -357,7 +419,7 @@
final ServiceRegistration registration = new ServiceRegistration(
mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
- NO_PACKET /* sentPacketCount */);
+ NO_PACKET /* sentPacketCount */, ttl);
mServices.put(serviceId, registration);
// Remove existing exiting service
@@ -366,34 +428,41 @@
}
/**
- * @return The ID of the service identified by its name, or -1 if none.
+ * @return The ID of the service identified by its name and type, or -1 if none.
*/
- private int getServiceByName(@NonNull String serviceName) {
+ private int getServiceByNameAndType(
+ @Nullable String serviceName, @Nullable String serviceType) {
+ if (TextUtils.isEmpty(serviceName) || TextUtils.isEmpty(serviceType)) {
+ return -1;
+ }
for (int i = 0; i < mServices.size(); i++) {
- final ServiceRegistration registration = mServices.valueAt(i);
- if (MdnsUtils.equalsIgnoreDnsCase(serviceName,
- registration.serviceInfo.getServiceName())) {
+ final NsdServiceInfo info = mServices.valueAt(i).serviceInfo;
+ if (MdnsUtils.equalsIgnoreDnsCase(serviceName, info.getServiceName())
+ && MdnsUtils.equalsIgnoreDnsCase(serviceType, info.getServiceType())) {
return mServices.keyAt(i);
}
}
return -1;
}
- private MdnsProber.ProbingInfo makeProbingInfo(int serviceId,
- @NonNull MdnsServiceRecord srvRecord,
- @NonNull List<MdnsInetAddressRecord> inetAddressRecords) {
+ private MdnsProber.ProbingInfo makeProbingInfo(
+ int serviceId, ServiceRegistration registration) {
final List<MdnsRecord> probingRecords = new ArrayList<>();
// Probe with cacheFlush cleared; it is set when announcing, as it was verified unique:
// RFC6762 10.2
- probingRecords.add(new MdnsServiceRecord(srvRecord.getName(),
- 0L /* receiptTimeMillis */,
- false /* cacheFlush */,
- srvRecord.getTtl(),
- srvRecord.getServicePriority(), srvRecord.getServiceWeight(),
- srvRecord.getServicePort(),
- srvRecord.getServiceHost()));
+ if (registration.srvRecord != null) {
+ MdnsServiceRecord srvRecord = registration.srvRecord.record;
+ probingRecords.add(new MdnsServiceRecord(srvRecord.getName(),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ srvRecord.getTtl(),
+ srvRecord.getServicePriority(), srvRecord.getServiceWeight(),
+ srvRecord.getServicePort(),
+ srvRecord.getServiceHost()));
+ }
- for (MdnsInetAddressRecord inetAddressRecord : inetAddressRecords) {
+ for (MdnsInetAddressRecord inetAddressRecord :
+ makeProbingInetAddressRecords(registration.serviceInfo)) {
probingRecords.add(new MdnsInetAddressRecord(inetAddressRecord.getName(),
0L /* receiptTimeMillis */,
false /* cacheFlush */,
@@ -490,6 +559,16 @@
return ret;
}
+ private boolean isTruncatedKnownAnswerPacket(MdnsPacket packet) {
+ if (!mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()
+ // Should ignore the response packet.
+ || (packet.flags & MdnsConstants.FLAGS_RESPONSE) != 0) {
+ return false;
+ }
+ // Check the packet contains no questions and as many more Known-Answer records as will fit.
+ return packet.questions.size() == 0 && packet.answers.size() != 0;
+ }
+
/**
* Get the reply to send to an incoming packet.
*
@@ -499,26 +578,37 @@
@Nullable
public MdnsReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
final long now = SystemClock.elapsedRealtime();
- final boolean replyUnicast = (packet.flags & MdnsConstants.QCLASS_UNICAST) != 0;
+
+ // TODO: b/322142420 - Set<RecordInfo<?>> may contain duplicate records wrapped in different
+ // RecordInfo<?>s when custom host is enabled.
// 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, additionalAnswerInfo, Collections.emptyList());
+ if (addReplyFromService(question, mGeneralRecords, null /* servicePtrRecord */,
+ null /* serviceSrvRecord */, null /* serviceTxtRecord */,
+ null /* hostname */,
+ 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,
+ registration.srvRecord, registration.txtRecord,
+ registration.serviceInfo.getHostname(),
+ replyUnicastEnabled, now,
answerInfo, additionalAnswerInfo, packet.answers)) {
+ replyUnicast &= question.isUnicastReplyRequested();
registration.repliedServiceCount++;
registration.sentPacketCount++;
}
@@ -534,7 +624,12 @@
final List<MdnsRecord> additionalAnswerRecords =
new ArrayList<>(additionalAnswerInfo.size());
for (RecordInfo<?> info : additionalAnswerInfo) {
- additionalAnswerRecords.add(info.record);
+ // Different RecordInfos may contain the same record.
+ // For example, when there are multiple services referring to the same custom host,
+ // there are multiple RecordInfos containing the same address record.
+ if (!additionalAnswerRecords.contains(info.record)) {
+ additionalAnswerRecords.add(info.record);
+ }
}
// RFC6762 6.1: negative responses
@@ -546,7 +641,20 @@
answerInfo.iterator(), additionalAnswerInfo.iterator());
if (answerInfo.size() == 0 && additionalAnswerRecords.size() == 0) {
- return null;
+ // RFC6762 7.2. Multipacket Known-Answer Suppression
+ // Sometimes a Multicast DNS querier will already have too many answers
+ // to fit in the Known-Answer Section of its query packets. In this
+ // case, it should issue a Multicast DNS query containing a question and
+ // as many Known-Answer records as will fit. It MUST then set the TC
+ // (Truncated) bit in the header before sending the query. It MUST
+ // immediately follow the packet with another query packet containing no
+ // questions and as many more Known-Answer records as will fit. If
+ // there are still too many records remaining to fit in the packet, it
+ // again sets the TC bit and continues until all the Known-Answer
+ // records have been sent.
+ if (!isTruncatedKnownAnswerPacket(packet)) {
+ return null;
+ }
}
// Determine the send delay
@@ -570,6 +678,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;
@@ -585,10 +699,14 @@
if (!replyUnicast) {
info.lastAdvertisedTimeMs = info.lastSentTimeMs;
}
- answerRecords.add(info.record);
+ // Different RecordInfos may the contain the same record
+ if (!answerRecords.contains(info.record)) {
+ answerRecords.add(info.record);
+ }
}
- return new MdnsReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest);
+ return new MdnsReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest, src,
+ new ArrayList<>(packet.answers));
}
private boolean isKnownAnswer(MdnsRecord answer, @NonNull List<MdnsRecord> knownAnswerRecords) {
@@ -608,7 +726,8 @@
@Nullable List<RecordInfo<MdnsPointerRecord>> servicePtrRecords,
@Nullable RecordInfo<MdnsServiceRecord> serviceSrvRecord,
@Nullable RecordInfo<MdnsTextRecord> serviceTxtRecord,
- boolean replyUnicast, long now, @NonNull Set<RecordInfo<?>> answerInfo,
+ @Nullable String hostname,
+ boolean replyUnicastEnabled, long now, @NonNull Set<RecordInfo<?>> answerInfo,
@NonNull Set<RecordInfo<?>> additionalAnswerInfo,
@NonNull List<MdnsRecord> knownAnswerRecords) {
boolean hasDnsSdPtrRecordAnswer = false;
@@ -648,7 +767,7 @@
// 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
+ if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()
&& isKnownAnswer(info.record, knownAnswerRecords)) {
continue;
}
@@ -659,7 +778,8 @@
// 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;
}
@@ -678,7 +798,7 @@
true /* cacheFlush */,
// TODO: RFC6762 6.1: "In general, the TTL given for an NSEC record SHOULD
// be the same as the TTL that the record would have had, had it existed."
- NAME_RECORDS_TTL_MILLIS,
+ DEFAULT_NAME_RECORDS_TTL_MILLIS,
question.getName(),
new int[] { question.getType() });
additionalAnswerInfo.add(
@@ -700,11 +820,7 @@
// RFC6763 12.1&.2: if including PTR or SRV record, include the address records it names
if (hasDnsSdPtrRecordAnswer || hasDnsSdSrvRecordAnswer) {
- for (RecordInfo<?> record : mGeneralRecords) {
- if (record.record instanceof MdnsInetAddressRecord) {
- additionalAnswerInfo.add(record);
- }
- }
+ additionalAnswerInfo.addAll(getInetAddressRecordsForHostname(hostname));
}
return true;
}
@@ -809,38 +925,112 @@
}
}
+ @Nullable
+ public String getHostnameForServiceId(int id) {
+ ServiceRegistration registration = mServices.get(id);
+ if (registration == null) {
+ return null;
+ }
+ return registration.serviceInfo.getHostname();
+ }
+
+ /**
+ * Restart probing the services which are being probed and using the given custom hostname.
+ *
+ * @return The list of {@link MdnsProber.ProbingInfo} to be used by advertiser.
+ */
+ public List<MdnsProber.ProbingInfo> restartProbingForHostname(@NonNull String hostname) {
+ final ArrayList<MdnsProber.ProbingInfo> probingInfos = new ArrayList<>();
+ forEachActiveServiceRegistrationWithHostname(
+ hostname,
+ (id, registration) -> {
+ if (!registration.isProbing) {
+ return;
+ }
+ probingInfos.add(makeProbingInfo(id, registration));
+ });
+ return probingInfos;
+ }
+
+ /**
+ * Restart announcing the services which are using the given custom hostname.
+ *
+ * @return The list of {@link MdnsAnnouncer.AnnouncementInfo} to be used by advertiser.
+ */
+ public List<MdnsAnnouncer.AnnouncementInfo> restartAnnouncingForHostname(
+ @NonNull String hostname) {
+ final ArrayList<MdnsAnnouncer.AnnouncementInfo> announcementInfos = new ArrayList<>();
+ forEachActiveServiceRegistrationWithHostname(
+ hostname,
+ (id, registration) -> {
+ if (registration.isProbing) {
+ return;
+ }
+ announcementInfos.add(makeAnnouncementInfo(id, registration));
+ });
+ return announcementInfos;
+ }
+
/**
* Called to indicate that probing succeeded for a service.
+ *
* @param probeSuccessInfo The successful probing info.
* @return The {@link MdnsAnnouncer.AnnouncementInfo} to send, now that probing has succeeded.
*/
public MdnsAnnouncer.AnnouncementInfo onProbingSucceeded(
- MdnsProber.ProbingInfo probeSuccessInfo)
- throws IOException {
-
- final ServiceRegistration registration = mServices.get(probeSuccessInfo.getServiceId());
- if (registration == null) throw new IOException(
- "Service is not registered: " + probeSuccessInfo.getServiceId());
+ MdnsProber.ProbingInfo probeSuccessInfo) throws IOException {
+ final int serviceId = probeSuccessInfo.getServiceId();
+ final ServiceRegistration registration = mServices.get(serviceId);
+ if (registration == null) {
+ throw new IOException("Service is not registered: " + serviceId);
+ }
registration.setProbing(false);
- final ArrayList<MdnsRecord> answers = new ArrayList<>();
+ return makeAnnouncementInfo(serviceId, registration);
+ }
+
+ /**
+ * Make the announcement info of the given service ID.
+ *
+ * @param serviceId The service ID.
+ * @param registration The service registration.
+ * @return The {@link MdnsAnnouncer.AnnouncementInfo} of the given service ID.
+ */
+ private MdnsAnnouncer.AnnouncementInfo makeAnnouncementInfo(
+ int serviceId, ServiceRegistration registration) {
+ final Set<MdnsRecord> answersSet = new LinkedHashSet<>();
final ArrayList<MdnsRecord> additionalAnswers = new ArrayList<>();
- // Interface address records in general records
- for (RecordInfo<?> record : mGeneralRecords) {
- answers.add(record.record);
+ // When using default host, add interface address records from general records
+ if (TextUtils.isEmpty(registration.serviceInfo.getHostname())) {
+ for (RecordInfo<?> record : mGeneralRecords) {
+ answersSet.add(record.record);
+ }
+ } else {
+ // TODO: b/321617573 - include PTR records for addresses
+ // The custom host may have more addresses in other registrations
+ forEachActiveServiceRegistrationWithHostname(
+ registration.serviceInfo.getHostname(),
+ (id, otherRegistration) -> {
+ if (otherRegistration.isProbing) {
+ return;
+ }
+ for (RecordInfo<?> addressRecordInfo : otherRegistration.addressRecords) {
+ answersSet.add(addressRecordInfo.record);
+ }
+ });
}
// All service records
for (RecordInfo<?> info : registration.allRecords) {
- answers.add(info.record);
+ answersSet.add(info.record);
}
addNsecRecordsForUniqueNames(additionalAnswers,
mGeneralRecords.iterator(), registration.allRecords.iterator());
- return new MdnsAnnouncer.AnnouncementInfo(probeSuccessInfo.getServiceId(),
- answers, additionalAnswers);
+ return new MdnsAnnouncer.AnnouncementInfo(serviceId,
+ new ArrayList<>(answersSet), additionalAnswers);
}
/**
@@ -859,8 +1049,13 @@
for (RecordInfo<MdnsPointerRecord> ptrRecord : registration.ptrRecords) {
answers.add(ptrRecord.record);
}
- answers.add(registration.srvRecord.record);
- answers.add(registration.txtRecord.record);
+ if (registration.srvRecord != null) {
+ answers.add(registration.srvRecord.record);
+ }
+ if (registration.txtRecord != null) {
+ answers.add(registration.txtRecord.record);
+ }
+ // TODO: Support custom host. It currently only supports default host.
for (RecordInfo<?> record : mGeneralRecords) {
if (record.record instanceof MdnsInetAddressRecord) {
answers.add(record.record);
@@ -875,70 +1070,181 @@
Collections.emptyList() /* additionalRecords */);
}
+ /** Check if the record is in any service registration */
+ private boolean hasInetAddressRecord(@NonNull MdnsInetAddressRecord record) {
+ for (int i = 0; i < mServices.size(); i++) {
+ final ServiceRegistration registration = mServices.valueAt(i);
+ if (registration.exiting) continue;
+
+ for (RecordInfo<MdnsInetAddressRecord> localRecord : registration.addressRecords) {
+ if (Objects.equals(localRecord.record, record)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
/**
* Get the service IDs of services conflicting with a received packet.
+ *
+ * <p>It returns a Map of service ID => conflict type. Conflict type is a bitmap telling which
+ * part of the service is conflicting. See {@link MdnsInterfaceAdvertiser#CONFLICT_SERVICE} and
+ * {@link MdnsInterfaceAdvertiser#CONFLICT_HOST}.
*/
- public Set<Integer> getConflictingServices(MdnsPacket packet) {
+ public Map<Integer, Integer> getConflictingServices(MdnsPacket packet) {
// Avoid allocating a new set for each incoming packet: use an empty set by default.
- Set<Integer> conflicting = Collections.emptySet();
+ Map<Integer, Integer> conflicting = Collections.emptyMap();
for (MdnsRecord record : packet.answers) {
for (int i = 0; i < mServices.size(); i++) {
final ServiceRegistration registration = mServices.valueAt(i);
if (registration.exiting) continue;
- // Only look for conflicts in service name, as a different service name can be used
- // if there is a conflict, but there is nothing actionable if any other conflict
- // happens. In fact probing is only done for the service name in the SRV record.
- // This means only SRV and TXT records need to be checked.
- final RecordInfo<MdnsServiceRecord> srvRecord = registration.srvRecord;
- if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(),
- srvRecord.record.getName())) {
- continue;
+ int conflictType = 0;
+
+ if (conflictForService(record, registration)) {
+ conflictType |= CONFLICT_SERVICE;
}
- // As per RFC6762 9., it's fine if the "conflict" is an identical record with same
- // data.
- if (record instanceof MdnsServiceRecord) {
- final MdnsServiceRecord local = srvRecord.record;
- final MdnsServiceRecord other = (MdnsServiceRecord) record;
- // Note "equals" does not consider TTL or receipt time, as intended here
- if (Objects.equals(local, other)) {
- continue;
+ if (conflictForHost(record, registration)) {
+ conflictType |= CONFLICT_HOST;
+ }
+
+ if (conflictType != 0) {
+ if (conflicting.isEmpty()) {
+ // Conflict was found: use a mutable set
+ conflicting = new ArrayMap<>();
}
+ final int serviceId = mServices.keyAt(i);
+ conflicting.put(serviceId, conflictType);
}
-
- if (record instanceof MdnsTextRecord) {
- final MdnsTextRecord local = registration.txtRecord.record;
- final MdnsTextRecord other = (MdnsTextRecord) record;
- if (Objects.equals(local, other)) {
- continue;
- }
- }
-
- if (conflicting.size() == 0) {
- // Conflict was found: use a mutable set
- conflicting = new ArraySet<>();
- }
- final int serviceId = mServices.keyAt(i);
- conflicting.add(serviceId);
}
}
return conflicting;
}
- private List<MdnsInetAddressRecord> makeProbingInetAddressRecords() {
- final List<MdnsInetAddressRecord> records = new ArrayList<>();
- if (mMdnsFeatureFlags.mIncludeInetAddressRecordsInProbing) {
- for (RecordInfo<?> record : mGeneralRecords) {
- if (record.record instanceof MdnsInetAddressRecord) {
- records.add((MdnsInetAddressRecord) record.record);
- }
+
+ private static boolean conflictForService(
+ @NonNull MdnsRecord record, @NonNull ServiceRegistration registration) {
+ if (registration.srvRecord == null) {
+ return false;
+ }
+
+ final RecordInfo<MdnsServiceRecord> srvRecord = registration.srvRecord;
+ if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(), srvRecord.record.getName())) {
+ return false;
+ }
+
+ // As per RFC6762 9., it's fine if the "conflict" is an identical record with same
+ // data.
+ if (record instanceof MdnsServiceRecord) {
+ final MdnsServiceRecord local = srvRecord.record;
+ final MdnsServiceRecord other = (MdnsServiceRecord) record;
+ // Note "equals" does not consider TTL or receipt time, as intended here
+ if (Objects.equals(local, other)) {
+ return false;
}
}
+
+ if (record instanceof MdnsTextRecord) {
+ final MdnsTextRecord local = registration.txtRecord.record;
+ final MdnsTextRecord other = (MdnsTextRecord) record;
+ if (Objects.equals(local, other)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean conflictForHost(
+ @NonNull MdnsRecord record, @NonNull ServiceRegistration registration) {
+ // Only custom hosts are checked. When using the default host, the hostname is derived from
+ // a UUID and it's supposed to be unique.
+ if (registration.serviceInfo.getHostname() == null) {
+ return false;
+ }
+
+ // The record's name cannot be registered by NsdManager so it's not a conflict.
+ if (record.getName().length != 2 || !record.getName()[1].equals(LOCAL_TLD)) {
+ return false;
+ }
+
+ // Different names. There won't be a conflict.
+ if (!MdnsUtils.equalsIgnoreDnsCase(
+ record.getName()[0], registration.serviceInfo.getHostname())) {
+ return false;
+ }
+
+ // If this registration has any address record and there's no identical record in the
+ // repository, it's a conflict. There will be no conflict if no registration has addresses
+ // for that hostname.
+ if (record instanceof MdnsInetAddressRecord) {
+ if (!registration.addressRecords.isEmpty()) {
+ return !hasInetAddressRecord((MdnsInetAddressRecord) record);
+ }
+ }
+
+ return false;
+ }
+
+ private List<RecordInfo<MdnsInetAddressRecord>> getInetAddressRecordsForHostname(
+ @Nullable String hostname) {
+ List<RecordInfo<MdnsInetAddressRecord>> records = new ArrayList<>();
+ if (TextUtils.isEmpty(hostname)) {
+ forEachAddressRecord(mGeneralRecords, records::add);
+ } else {
+ forEachActiveServiceRegistrationWithHostname(
+ hostname,
+ (id, service) -> {
+ if (service.isProbing) return;
+ records.addAll(service.addressRecords);
+ });
+ }
return records;
}
+ private List<MdnsInetAddressRecord> makeProbingInetAddressRecords(
+ @NonNull NsdServiceInfo serviceInfo) {
+ final List<MdnsInetAddressRecord> records = new ArrayList<>();
+ if (TextUtils.isEmpty(serviceInfo.getHostname())) {
+ if (mMdnsFeatureFlags.mIncludeInetAddressRecordsInProbing) {
+ forEachAddressRecord(mGeneralRecords, r -> records.add(r.record));
+ }
+ } else {
+ forEachActiveServiceRegistrationWithHostname(
+ serviceInfo.getHostname(),
+ (id, service) -> {
+ for (RecordInfo<MdnsInetAddressRecord> recordInfo :
+ service.addressRecords) {
+ records.add(recordInfo.record);
+ }
+ });
+ }
+ return records;
+ }
+
+ private static void forEachAddressRecord(
+ List<RecordInfo<?>> records, Consumer<RecordInfo<MdnsInetAddressRecord>> consumer) {
+ for (RecordInfo<?> record : records) {
+ if (record.record instanceof MdnsInetAddressRecord) {
+ consumer.accept((RecordInfo<MdnsInetAddressRecord>) record);
+ }
+ }
+ }
+
+ private void forEachActiveServiceRegistrationWithHostname(
+ @NonNull String hostname, BiConsumer<Integer, ServiceRegistration> consumer) {
+ for (int i = 0; i < mServices.size(); ++i) {
+ int id = mServices.keyAt(i);
+ ServiceRegistration service = mServices.valueAt(i);
+ if (service.exiting) continue;
+ if (MdnsUtils.equalsIgnoreDnsCase(service.serviceInfo.getHostname(), hostname)) {
+ consumer.accept(id, service);
+ }
+ }
+ }
+
/**
* (Re)set a service to the probing state.
* @return The {@link MdnsProber.ProbingInfo} to send for probing.
@@ -949,8 +1255,8 @@
if (registration == null) return null;
registration.setProbing(true);
- return makeProbingInfo(
- serviceId, registration.srvRecord.record, makeProbingInetAddressRecords());
+
+ return makeProbingInfo(serviceId, registration);
}
/**
@@ -984,10 +1290,9 @@
if (existing == null) return null;
final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
- existing.repliedServiceCount, existing.sentPacketCount);
+ existing.repliedServiceCount, existing.sentPacketCount, existing.ttl);
mServices.put(serviceId, newService);
- return makeProbingInfo(
- serviceId, newService.srvRecord.record, makeProbingInetAddressRecords());
+ return makeProbingInfo(serviceId, newService);
}
/**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
index ce61b54..8747f67 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
@@ -32,22 +32,32 @@
public final long sendDelayMs;
@NonNull
public final InetSocketAddress destination;
+ @NonNull
+ public final InetSocketAddress source;
+ @NonNull
+ public final List<MdnsRecord> knownAnswers;
public MdnsReplyInfo(
@NonNull List<MdnsRecord> answers,
@NonNull List<MdnsRecord> additionalAnswers,
long sendDelayMs,
- @NonNull InetSocketAddress destination) {
+ @NonNull InetSocketAddress destination,
+ @NonNull InetSocketAddress source,
+ @NonNull List<MdnsRecord> knownAnswers) {
this.answers = answers;
this.additionalAnswers = additionalAnswers;
this.sendDelayMs = sendDelayMs;
this.destination = destination;
+ this.source = source;
+ this.knownAnswers = knownAnswers;
}
@Override
public String toString() {
- return "{MdnsReplyInfo to " + destination + ", answers: " + answers.size()
+ return "{MdnsReplyInfo: " + source + " to " + destination
+ + ", answers: " + answers.size()
+ ", additionalAnswers: " + additionalAnswers.size()
+ + ", knownAnswers: " + knownAnswers.size()
+ ", sendDelayMs " + sendDelayMs + "}";
}
}
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 651b643..db3845a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -16,6 +16,8 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
import android.annotation.NonNull;
@@ -24,6 +26,8 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.util.ArrayMap;
+import android.util.ArraySet;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.SharedLog;
@@ -35,7 +39,10 @@
import java.net.Inet6Address;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
/**
* A class that handles sending mDNS replies to a {@link MulticastSocket}, possibly queueing them
@@ -60,6 +67,12 @@
private final boolean mEnableDebugLog;
@NonNull
private final Dependencies mDependencies;
+ // RFC6762 15.2. Multipacket Known-Answer lists
+ // Multicast DNS responders associate the initial truncated query with its
+ // continuation packets by examining the source IP address in each packet.
+ private final Map<InetSocketAddress, MdnsReplyInfo> mSrcReplies = new ArrayMap<>();
+ @NonNull
+ private final MdnsFeatureFlags mMdnsFeatureFlags;
/**
* Dependencies of MdnsReplySender, for injection in tests.
@@ -80,24 +93,50 @@
public void removeMessages(@NonNull Handler handler, int what) {
handler.removeMessages(what);
}
+
+ /**
+ * @see Handler#removeMessages(int)
+ */
+ public void removeMessages(@NonNull Handler handler, int what, @NonNull Object object) {
+ handler.removeMessages(what, object);
+ }
}
public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
@NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
- boolean enableDebugLog) {
- this(looper, socket, packetCreationBuffer, sharedLog, enableDebugLog, new Dependencies());
+ boolean enableDebugLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+ this(looper, socket, packetCreationBuffer, sharedLog, enableDebugLog, new Dependencies(),
+ mdnsFeatureFlags);
}
@VisibleForTesting
public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
@NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
- boolean enableDebugLog, @NonNull Dependencies dependencies) {
+ boolean enableDebugLog, @NonNull Dependencies dependencies,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
mHandler = new SendHandler(looper);
mSocket = socket;
mPacketCreationBuffer = packetCreationBuffer;
mSharedLog = sharedLog;
mEnableDebugLog = enableDebugLog;
mDependencies = dependencies;
+ mMdnsFeatureFlags = mdnsFeatureFlags;
+ }
+
+ static InetSocketAddress getReplyDestination(@NonNull InetSocketAddress queuingDest,
+ @NonNull InetSocketAddress incomingDest) {
+ // The queuing reply is multicast, just use the current destination.
+ if (queuingDest.equals(IPV4_SOCKET_ADDR) || queuingDest.equals(IPV6_SOCKET_ADDR)) {
+ return queuingDest;
+ }
+
+ // The incoming reply is multicast, change the reply from unicast to multicast since
+ // replying unicast when the query requests unicast reply is optional.
+ if (incomingDest.equals(IPV4_SOCKET_ADDR) || incomingDest.equals(IPV6_SOCKET_ADDR)) {
+ return incomingDest;
+ }
+
+ return queuingDest;
}
/**
@@ -105,9 +144,53 @@
*/
public void queueReply(@NonNull MdnsReplyInfo reply) {
ensureRunningOnHandlerThread(mHandler);
- // TODO: implement response aggregation (RFC 6762 6.4)
- mDependencies.sendMessageDelayed(
- mHandler, mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
+
+ if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()) {
+ mDependencies.removeMessages(mHandler, MSG_SEND, reply.source);
+
+ final MdnsReplyInfo queuingReply = mSrcReplies.remove(reply.source);
+ final ArraySet<MdnsRecord> answers = new ArraySet<>();
+ final Set<MdnsRecord> additionalAnswers = new ArraySet<>();
+ final Set<MdnsRecord> knownAnswers = new ArraySet<>();
+ if (queuingReply != null) {
+ answers.addAll(queuingReply.answers);
+ additionalAnswers.addAll(queuingReply.additionalAnswers);
+ knownAnswers.addAll(queuingReply.knownAnswers);
+ }
+ answers.addAll(reply.answers);
+ additionalAnswers.addAll(reply.additionalAnswers);
+ knownAnswers.addAll(reply.knownAnswers);
+ // RFC6762 7.2. Multipacket Known-Answer Suppression
+ // If the responder sees any of its answers listed in the Known-Answer
+ // lists of subsequent packets from the querying host, it MUST delete
+ // that answer from the list of answers it is planning to give.
+ for (MdnsRecord knownAnswer : knownAnswers) {
+ final int idx = answers.indexOf(knownAnswer);
+ if (idx >= 0 && knownAnswer.getTtl() > answers.valueAt(idx).getTtl() / 2) {
+ answers.removeAt(idx);
+ }
+ }
+
+ if (answers.size() == 0) {
+ return;
+ }
+
+ final MdnsReplyInfo newReply = new MdnsReplyInfo(
+ new ArrayList<>(answers),
+ new ArrayList<>(additionalAnswers),
+ reply.sendDelayMs,
+ queuingReply == null ? reply.destination
+ : getReplyDestination(queuingReply.destination, reply.destination),
+ reply.source,
+ new ArrayList<>(knownAnswers));
+
+ mSrcReplies.put(newReply.source, newReply);
+ mDependencies.sendMessageDelayed(mHandler,
+ mHandler.obtainMessage(MSG_SEND, newReply.source), newReply.sendDelayMs);
+ } else {
+ mDependencies.sendMessageDelayed(
+ mHandler, mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
+ }
if (mEnableDebugLog) {
mSharedLog.v("Scheduling " + reply);
@@ -147,7 +230,21 @@
@Override
public void handleMessage(@NonNull Message msg) {
- final MdnsReplyInfo replyInfo = (MdnsReplyInfo) msg.obj;
+ final MdnsReplyInfo replyInfo;
+ if (mMdnsFeatureFlags.isKnownAnswerSuppressionEnabled()) {
+ // Retrieve the MdnsReplyInfo from the map via a source address, as the reply info
+ // will be combined or updated.
+ final InetSocketAddress source = (InetSocketAddress) msg.obj;
+ replyInfo = mSrcReplies.remove(source);
+ } else {
+ replyInfo = (MdnsReplyInfo) msg.obj;
+ }
+
+ if (replyInfo == null) {
+ mSharedLog.wtf("Unknown reply info.");
+ return;
+ }
+
if (mEnableDebugLog) mSharedLog.v("Sending " + replyInfo);
final int flags = 0x8400; // Response, authoritative (rfc6762 18.4)
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
index 63835d9..73405ab 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
@@ -39,6 +39,15 @@
* @hide
*/
public class MdnsSearchOptions implements Parcelable {
+ // Passive query mode scans less frequently in order to conserve battery and produce less
+ // network traffic.
+ public static final int PASSIVE_QUERY_MODE = 0;
+ // Active query mode scans frequently.
+ public static final int ACTIVE_QUERY_MODE = 1;
+ // Aggressive query mode scans more frequently than the active mode at first, and sends both
+ // unicast and multicast queries simultaneously, but in long sessions it eventually sends as
+ // many queries as the PASSIVE mode.
+ public static final int AGGRESSIVE_QUERY_MODE = 2;
/** @hide */
public static final Parcelable.Creator<MdnsSearchOptions> CREATOR =
@@ -47,9 +56,10 @@
public MdnsSearchOptions createFromParcel(Parcel source) {
return new MdnsSearchOptions(
source.createStringArrayList(),
- source.readInt() == 1,
+ source.readInt(),
source.readInt() == 1,
source.readParcelable(null),
+ source.readInt(),
source.readString(),
source.readInt() == 1,
source.readInt());
@@ -64,19 +74,22 @@
private final List<String> subtypes;
@Nullable
private final String resolveInstanceName;
- private final boolean isPassiveMode;
+ private final int queryMode;
private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
private final int numOfQueriesBeforeBackoff;
private final boolean removeExpiredService;
// The target network for searching. Null network means search on all possible interfaces.
@Nullable private final Network mNetwork;
+ // If the target interface does not have a Network, set to the interface index, otherwise unset.
+ private final int mInterfaceIndex;
/** Parcelable constructs for a {@link MdnsSearchOptions}. */
MdnsSearchOptions(
List<String> subtypes,
- boolean isPassiveMode,
+ int queryMode,
boolean removeExpiredService,
@Nullable Network network,
+ int interfaceIndex,
@Nullable String resolveInstanceName,
boolean onlyUseIpv6OnIpv6OnlyNetworks,
int numOfQueriesBeforeBackoff) {
@@ -84,11 +97,12 @@
if (subtypes != null) {
this.subtypes.addAll(subtypes);
}
- this.isPassiveMode = isPassiveMode;
+ this.queryMode = queryMode;
this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
this.removeExpiredService = removeExpiredService;
mNetwork = network;
+ mInterfaceIndex = interfaceIndex;
this.resolveInstanceName = resolveInstanceName;
}
@@ -111,11 +125,10 @@
}
/**
- * @return {@code true} if the passive mode is used. The passive mode scans less frequently in
- * order to conserve battery and produce less network traffic.
+ * @return the current query mode.
*/
- public boolean isPassiveMode() {
- return isPassiveMode;
+ public int getQueryMode() {
+ return queryMode;
}
/**
@@ -140,15 +153,27 @@
}
/**
- * Returns the network which the mdns query should target on.
+ * Returns the network which the mdns query should target.
*
- * @return the target network or null if search on all possible interfaces.
+ * @return the target network or null to search on all possible interfaces.
*/
@Nullable
public Network getNetwork() {
return mNetwork;
}
+
+ /**
+ * Returns the interface index which the mdns query should target.
+ *
+ * This is only set when the service is to be searched on an interface that does not have a
+ * Network, in which case {@link #getNetwork()} returns null.
+ * The interface index as per {@link java.net.NetworkInterface#getIndex}, or 0 if unset.
+ */
+ public int getInterfaceIndex() {
+ return mInterfaceIndex;
+ }
+
/**
* If non-null, queries should try to resolve all records of this specific service, rather than
* discovering all services.
@@ -166,9 +191,10 @@
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeStringList(subtypes);
- out.writeInt(isPassiveMode ? 1 : 0);
+ out.writeInt(queryMode);
out.writeInt(removeExpiredService ? 1 : 0);
out.writeParcelable(mNetwork, 0);
+ out.writeInt(mInterfaceIndex);
out.writeString(resolveInstanceName);
out.writeInt(onlyUseIpv6OnIpv6OnlyNetworks ? 1 : 0);
out.writeInt(numOfQueriesBeforeBackoff);
@@ -177,11 +203,12 @@
/** A builder to create {@link MdnsSearchOptions}. */
public static final class Builder {
private final Set<String> subtypes;
- private boolean isPassiveMode = true;
+ private int queryMode = PASSIVE_QUERY_MODE;
private boolean onlyUseIpv6OnIpv6OnlyNetworks = false;
private int numOfQueriesBeforeBackoff = 3;
private boolean removeExpiredService;
private Network mNetwork;
+ private int mInterfaceIndex;
private String resolveInstanceName;
private Builder() {
@@ -212,14 +239,12 @@
}
/**
- * Sets if the passive mode scan should be used. The passive mode scans less frequently in
- * order to conserve battery and produce less network traffic.
+ * Sets which query mode should be used.
*
- * @param isPassiveMode If set to {@code true}, passive mode will be used. If set to {@code
- * false}, active mode will be used.
+ * @param queryMode the query mode should be used.
*/
- public Builder setIsPassiveMode(boolean isPassiveMode) {
- this.isPassiveMode = isPassiveMode;
+ public Builder setQueryMode(int queryMode) {
+ this.queryMode = queryMode;
return this;
}
@@ -272,13 +297,24 @@
return this;
}
+ /**
+ * Set the interface index to use for the query, if not querying on a {@link Network}.
+ *
+ * @see MdnsSearchOptions#getInterfaceIndex()
+ */
+ public Builder setInterfaceIndex(int index) {
+ mInterfaceIndex = index;
+ return this;
+ }
+
/** Builds a {@link MdnsSearchOptions} with the arguments supplied to this builder. */
public MdnsSearchOptions build() {
return new MdnsSearchOptions(
new ArrayList<>(subtypes),
- isPassiveMode,
+ queryMode,
removeExpiredService,
mNetwork,
+ mInterfaceIndex,
resolveInstanceName,
onlyUseIpv6OnIpv6OnlyNetworks,
numOfQueriesBeforeBackoff);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
index 78df6df..1ec9e39 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -28,6 +28,7 @@
import com.android.net.module.util.ByteUtils;
import java.nio.charset.Charset;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -62,7 +63,8 @@
source.createStringArrayList(),
source.createTypedArrayList(TextEntry.CREATOR),
source.readInt(),
- source.readParcelable(null));
+ source.readParcelable(Network.class.getClassLoader()),
+ Instant.ofEpochSecond(source.readLong()));
}
@Override
@@ -89,54 +91,8 @@
@Nullable
private final Network network;
- /** Constructs a {@link MdnsServiceInfo} object with default values. */
- public MdnsServiceInfo(
- String serviceInstanceName,
- String[] serviceType,
- @Nullable List<String> subtypes,
- String[] hostName,
- int port,
- @Nullable String ipv4Address,
- @Nullable String ipv6Address,
- @Nullable List<String> textStrings) {
- this(
- serviceInstanceName,
- serviceType,
- subtypes,
- hostName,
- port,
- List.of(ipv4Address),
- List.of(ipv6Address),
- textStrings,
- /* textEntries= */ null,
- /* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
- /* network= */ null);
- }
-
- /** Constructs a {@link MdnsServiceInfo} object with default values. */
- public MdnsServiceInfo(
- String serviceInstanceName,
- String[] serviceType,
- List<String> subtypes,
- String[] hostName,
- int port,
- @Nullable String ipv4Address,
- @Nullable String ipv6Address,
- @Nullable List<String> textStrings,
- @Nullable List<TextEntry> textEntries) {
- this(
- serviceInstanceName,
- serviceType,
- subtypes,
- hostName,
- port,
- List.of(ipv4Address),
- List.of(ipv6Address),
- textStrings,
- textEntries,
- /* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
- /* network= */ null);
- }
+ @NonNull
+ private final Instant expirationTime;
/**
* Constructs a {@link MdnsServiceInfo} object with default values.
@@ -165,7 +121,8 @@
textStrings,
textEntries,
interfaceIndex,
- /* network= */ null);
+ /* network= */ null,
+ /* expirationTime= */ Instant.MAX);
}
/**
@@ -184,7 +141,8 @@
@Nullable List<String> textStrings,
@Nullable List<TextEntry> textEntries,
int interfaceIndex,
- @Nullable Network network) {
+ @Nullable Network network,
+ @NonNull Instant expirationTime) {
this.serviceInstanceName = serviceInstanceName;
this.serviceType = serviceType;
this.subtypes = new ArrayList<>();
@@ -217,6 +175,7 @@
this.attributes = Collections.unmodifiableMap(attributes);
this.interfaceIndex = interfaceIndex;
this.network = network;
+ this.expirationTime = Instant.ofEpochSecond(expirationTime.getEpochSecond());
}
private static List<TextEntry> parseTextStrings(List<String> textStrings) {
@@ -314,6 +273,17 @@
}
/**
+ * Returns the timestamp after when this service is expired or {@code null} if the expiration
+ * time is unknown.
+ *
+ * A service is considered expired if any of its DNS record is expired.
+ */
+ @NonNull
+ public Instant getExpirationTime() {
+ return expirationTime;
+ }
+
+ /**
* Returns attribute value for {@code key} as a UTF-8 string. It's the caller who must make sure
* that the value of {@code key} is indeed a UTF-8 string. {@code null} will be returned if no
* attribute value exists for {@code key}.
@@ -364,6 +334,7 @@
out.writeTypedList(textEntries);
out.writeInt(interfaceIndex);
out.writeParcelable(network, 0);
+ out.writeLong(expirationTime.getEpochSecond());
}
@Override
@@ -377,7 +348,8 @@
+ ", interfaceIndex: " + interfaceIndex
+ ", network: " + network
+ ", textStrings: " + textStrings
- + ", textEntries: " + textEntries;
+ + ", textEntries: " + textEntries
+ + ", expirationTime: " + expirationTime;
}
@@ -496,4 +468,4 @@
out.writeByteArray(value);
}
}
-}
\ No newline at end of file
+}
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 df0a040..b3bdbe0 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -30,15 +30,22 @@
import android.util.ArrayMap;
import android.util.Pair;
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
+
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.DatagramPacket;
import java.net.Inet4Address;
import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@@ -52,7 +59,6 @@
public class MdnsServiceTypeClient {
private static final String TAG = MdnsServiceTypeClient.class.getSimpleName();
- private static final int DEFAULT_MTU = 1500;
@VisibleForTesting
static final int EVENT_START_QUERYTASK = 1;
static final int EVENT_QUERY_RESULT = 2;
@@ -81,7 +87,8 @@
notifyRemovedServiceToListeners(previousResponse, "Service record expired");
}
};
- private final ArrayMap<MdnsServiceBrowserListener, MdnsSearchOptions> listeners =
+ @NonNull private final MdnsFeatureFlags featureFlags;
+ private final ArrayMap<MdnsServiceBrowserListener, ListenerInfo> listeners =
new ArrayMap<>();
private final boolean removeServiceAfterTtlExpires =
MdnsConfigs.removeServiceAfterTtlExpires();
@@ -95,6 +102,32 @@
private long currentSessionId = 0;
private long lastSentTime;
+ private static class ListenerInfo {
+ @NonNull
+ final MdnsSearchOptions searchOptions;
+ final Set<String> discoveredServiceNames;
+
+ ListenerInfo(@NonNull MdnsSearchOptions searchOptions,
+ @Nullable ListenerInfo previousInfo) {
+ this.searchOptions = searchOptions;
+ this.discoveredServiceNames = previousInfo == null
+ ? MdnsUtils.newSet() : previousInfo.discoveredServiceNames;
+ }
+
+ /**
+ * Set the given service name as discovered.
+ *
+ * @return true if the service name was not discovered before.
+ */
+ boolean setServiceDiscovered(@NonNull String serviceName) {
+ return discoveredServiceNames.add(MdnsUtils.toDnsLowerCase(serviceName));
+ }
+
+ void unsetServiceDiscovered(@NonNull String serviceName) {
+ discoveredServiceNames.remove(MdnsUtils.toDnsLowerCase(serviceName));
+ }
+ }
+
private class QueryTaskHandler extends Handler {
QueryTaskHandler(Looper looper) {
super(looper);
@@ -113,7 +146,8 @@
// before sending the query, it needs to be called just before sending it.
final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
final QueryTask queryTask = new QueryTask(taskArgs, servicesToResolve,
- servicesToResolve.size() < listeners.size() /* sendDiscoveryQueries */);
+ getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners),
+ getExistingServices());
executor.submit(queryTask);
break;
}
@@ -162,7 +196,7 @@
/**
* Dependencies of MdnsServiceTypeClient, for injection in tests.
*/
- @VisibleForTesting
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static class Dependencies {
/**
* @see Handler#sendMessageDelayed(Message, long)
@@ -192,6 +226,25 @@
public void sendMessage(@NonNull Handler handler, @NonNull Message message) {
handler.sendMessage(message);
}
+
+ /**
+ * Generate the DatagramPackets from given MdnsPacket and InetSocketAddress.
+ *
+ * <p> If the query with known answer feature is enabled and the MdnsPacket is too large for
+ * a single DatagramPacket, it will be split into multiple DatagramPackets.
+ */
+ public List<DatagramPacket> getDatagramPacketsFromMdnsPacket(
+ @NonNull byte[] packetCreationBuffer, @NonNull MdnsPacket packet,
+ @NonNull InetSocketAddress address, boolean isQueryWithKnownAnswer)
+ throws IOException {
+ if (isQueryWithKnownAnswer) {
+ return MdnsUtils.createQueryDatagramPackets(packetCreationBuffer, packet, address);
+ } else {
+ final byte[] queryBuffer =
+ MdnsUtils.createRawDnsPacket(packetCreationBuffer, packet);
+ return List.of(new DatagramPacket(queryBuffer, 0, queryBuffer.length, address));
+ }
+ }
}
/**
@@ -207,9 +260,10 @@
@NonNull SocketKey socketKey,
@NonNull SharedLog sharedLog,
@NonNull Looper looper,
- @NonNull MdnsServiceCache serviceCache) {
+ @NonNull MdnsServiceCache serviceCache,
+ @NonNull MdnsFeatureFlags featureFlags) {
this(serviceType, socketClient, executor, new Clock(), socketKey, sharedLog, looper,
- new Dependencies(), serviceCache);
+ new Dependencies(), serviceCache, featureFlags);
}
@VisibleForTesting
@@ -222,7 +276,8 @@
@NonNull SharedLog sharedLog,
@NonNull Looper looper,
@NonNull Dependencies dependencies,
- @NonNull MdnsServiceCache serviceCache) {
+ @NonNull MdnsServiceCache serviceCache,
+ @NonNull MdnsFeatureFlags featureFlags) {
this.serviceType = serviceType;
this.socketClient = socketClient;
this.executor = executor;
@@ -236,6 +291,7 @@
this.serviceCache = serviceCache;
this.mdnsQueryScheduler = new MdnsQueryScheduler();
this.cacheKey = new MdnsServiceCache.CacheKey(serviceType, socketKey);
+ this.featureFlags = featureFlags;
}
/**
@@ -281,6 +337,7 @@
textStrings = response.getTextRecord().getStrings();
textEntries = response.getTextRecord().getEntries();
}
+ Instant now = Instant.now();
// TODO: Throw an error message if response doesn't have Inet6 or Inet4 address.
return new MdnsServiceInfo(
serviceInstanceName,
@@ -293,7 +350,13 @@
textStrings,
textEntries,
response.getInterfaceIndex(),
- response.getNetwork());
+ response.getNetwork(),
+ now.plusMillis(response.getMinRemainingTtl(now.toEpochMilli())));
+ }
+
+ private List<MdnsResponse> getExistingServices() {
+ return featureFlags.isQueryWithKnownAnswerEnabled()
+ ? serviceCache.getCachedServices(cacheKey) : Collections.emptyList();
}
/**
@@ -311,12 +374,16 @@
ensureRunningOnHandlerThread(handler);
this.searchOptions = searchOptions;
boolean hadReply = false;
- if (listeners.put(listener, searchOptions) == null) {
+ final ListenerInfo existingInfo = listeners.get(listener);
+ final ListenerInfo listenerInfo = new ListenerInfo(searchOptions, existingInfo);
+ listeners.put(listener, listenerInfo);
+ if (existingInfo == null) {
for (MdnsResponse existingResponse : serviceCache.getCachedServices(cacheKey)) {
if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
final MdnsServiceInfo info =
buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
listener.onServiceNameDiscovered(info, true /* isServiceFromCache */);
+ listenerInfo.setServiceDiscovered(info.getServiceInstanceName());
if (existingResponse.isComplete()) {
listener.onServiceFound(info, true /* isServiceFromCache */);
hadReply = true;
@@ -329,8 +396,7 @@
// Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
// interested anymore.
final QueryTaskConfig taskConfig = new QueryTaskConfig(
- searchOptions.getSubtypes(),
- searchOptions.isPassiveMode(),
+ searchOptions.getQueryMode(),
searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
searchOptions.numOfQueriesBeforeBackoff(),
socketKey);
@@ -357,13 +423,23 @@
final QueryTask queryTask = new QueryTask(
mdnsQueryScheduler.scheduleFirstRun(taskConfig, now,
minRemainingTtl, currentSessionId), servicesToResolve,
- servicesToResolve.size() < listeners.size() /* sendDiscoveryQueries */);
+ getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners),
+ getExistingServices());
executor.submit(queryTask);
}
serviceCache.registerServiceExpiredCallback(cacheKey, serviceExpiredCallback);
}
+ private Set<String> getAllDiscoverySubtypes() {
+ final Set<String> subtypes = MdnsUtils.newSet();
+ for (int i = 0; i < listeners.size(); i++) {
+ final MdnsSearchOptions listenerOptions = listeners.valueAt(i).searchOptions;
+ subtypes.addAll(listenerOptions.getSubtypes());
+ }
+ return subtypes;
+ }
+
/**
* Get the executor service.
*/
@@ -480,9 +556,10 @@
private void notifyRemovedServiceToListeners(@NonNull MdnsResponse response,
@NonNull String message) {
for (int i = 0; i < listeners.size(); i++) {
- if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
+ if (!responseMatchesOptions(response, listeners.valueAt(i).searchOptions)) continue;
final MdnsServiceBrowserListener listener = listeners.keyAt(i);
if (response.getServiceInstanceName() != null) {
+ listeners.valueAt(i).unsetServiceDiscovered(response.getServiceInstanceName());
final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
response, serviceTypeLabels);
if (response.isComplete()) {
@@ -511,10 +588,9 @@
final MdnsResponse currentResponse =
serviceCache.getCachedService(serviceInstanceName, cacheKey);
- boolean newServiceFound = false;
+ final boolean newInCache = currentResponse == null;
boolean serviceBecomesComplete = false;
- if (currentResponse == null) {
- newServiceFound = true;
+ if (newInCache) {
if (serviceInstanceName != null) {
serviceCache.addOrUpdateService(cacheKey, response);
}
@@ -525,25 +601,28 @@
serviceBecomesComplete = !before && after;
}
sharedLog.i(String.format(
- "Handling response from service: %s, newServiceFound: %b, serviceBecomesComplete:"
+ "Handling response from service: %s, newInCache: %b, serviceBecomesComplete:"
+ " %b, responseIsComplete: %b",
- serviceInstanceName, newServiceFound, serviceBecomesComplete,
+ serviceInstanceName, newInCache, serviceBecomesComplete,
response.isComplete()));
MdnsServiceInfo serviceInfo =
buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
for (int i = 0; i < listeners.size(); i++) {
- if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
+ // If a service stops matching the options (currently can only happen if it loses a
+ // subtype), service lost callbacks should also be sent; this is not done today as
+ // only expiration of SRV records is used, not PTR records used for subtypes, so
+ // services never lose PTR record subtypes.
+ if (!responseMatchesOptions(response, listeners.valueAt(i).searchOptions)) continue;
final MdnsServiceBrowserListener listener = listeners.keyAt(i);
+ final ListenerInfo listenerInfo = listeners.valueAt(i);
+ final boolean newServiceFound = listenerInfo.setServiceDiscovered(serviceInstanceName);
if (newServiceFound) {
sharedLog.log("onServiceNameDiscovered: " + serviceInfo);
listener.onServiceNameDiscovered(serviceInfo, false /* isServiceFromCache */);
}
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 */);
@@ -571,18 +650,17 @@
return searchOptions != null && searchOptions.removeExpiredService();
}
- @VisibleForTesting
- MdnsPacketWriter createMdnsPacketWriter() {
- return new MdnsPacketWriter(DEFAULT_MTU);
- }
-
private List<MdnsResponse> makeResponsesForResolve(@NonNull SocketKey socketKey) {
final List<MdnsResponse> resolveResponses = new ArrayList<>();
for (int i = 0; i < listeners.size(); i++) {
- final String resolveName = listeners.valueAt(i).getResolveInstanceName();
+ final String resolveName = listeners.valueAt(i).searchOptions.getResolveInstanceName();
if (resolveName == null) {
continue;
}
+ if (CollectionUtils.any(resolveResponses,
+ r -> MdnsUtils.equalsIgnoreDnsCase(resolveName, r.getServiceInstanceName()))) {
+ continue;
+ }
MdnsResponse knownResponse =
serviceCache.getCachedService(resolveName, cacheKey);
if (knownResponse == null) {
@@ -599,6 +677,17 @@
return resolveResponses;
}
+ private static boolean needSendDiscoveryQueries(
+ @NonNull ArrayMap<MdnsServiceBrowserListener, ListenerInfo> listeners) {
+ // Note iterators are discouraged on ArrayMap as per its documentation
+ for (int i = 0; i < listeners.size(); i++) {
+ if (listeners.valueAt(i).searchOptions.getResolveInstanceName() == null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void tryRemoveServiceAfterTtlExpires() {
if (!shouldRemoveServiceAfterTtlExpires()) return;
@@ -631,12 +720,18 @@
private class QueryTask implements Runnable {
private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
private final List<MdnsResponse> servicesToResolve = new ArrayList<>();
+ private final List<String> subtypes = new ArrayList<>();
private final boolean sendDiscoveryQueries;
+ private final List<MdnsResponse> existingServices = new ArrayList<>();
QueryTask(@NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs,
- @NonNull List<MdnsResponse> servicesToResolve, boolean sendDiscoveryQueries) {
+ @NonNull Collection<MdnsResponse> servicesToResolve,
+ @NonNull Collection<String> subtypes, boolean sendDiscoveryQueries,
+ @NonNull Collection<MdnsResponse> existingServices) {
this.taskArgs = taskArgs;
this.servicesToResolve.addAll(servicesToResolve);
+ this.subtypes.addAll(subtypes);
this.sendDiscoveryQueries = sendDiscoveryQueries;
+ this.existingServices.addAll(existingServices);
}
@Override
@@ -646,9 +741,8 @@
result =
new EnqueueMdnsQueryCallable(
socketClient,
- createMdnsPacketWriter(),
serviceType,
- taskArgs.config.subtypes,
+ subtypes,
taskArgs.config.expectUnicastResponse,
taskArgs.config.transactionId,
taskArgs.config.socketKey,
@@ -656,11 +750,14 @@
sendDiscoveryQueries,
servicesToResolve,
clock,
- sharedLog)
+ sharedLog,
+ dependencies,
+ existingServices,
+ featureFlags.isQueryWithKnownAnswerEnabled())
.call();
} catch (RuntimeException e) {
sharedLog.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s",
- TextUtils.join(",", taskArgs.config.subtypes)), e);
+ TextUtils.join(",", subtypes)), e);
result = Pair.create(INVALID_TRANSACTION_ID, new ArrayList<>());
}
dependencies.sendMessage(
@@ -695,4 +792,13 @@
args.sessionId, timeToNextTasksWithBackoffInMs));
return timeToNextTasksWithBackoffInMs;
}
+
+ /**
+ * Dump ServiceTypeClient state.
+ */
+ public void dump(PrintWriter pw) {
+ ensureRunningOnHandlerThread(handler);
+ pw.println("ServiceTypeClient: Type{" + serviceType + "} " + socketKey + " with "
+ + listeners.size() + " listeners.");
+ }
}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocket.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocket.java
index c51811b..653ea6c 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocket.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocket.java
@@ -58,7 +58,6 @@
MdnsSocket(@NonNull MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider,
MulticastSocket multicastSocket, SharedLog sharedLog) throws IOException {
this.multicastNetworkInterfaceProvider = multicastNetworkInterfaceProvider;
- this.multicastNetworkInterfaceProvider.startWatchingConnectivityChanges();
this.multicastSocket = multicastSocket;
this.sharedLog = sharedLog;
// RFC Spec: https://tools.ietf.org/html/rfc6762
@@ -120,7 +119,6 @@
public void close() {
// This is a race with the use of the file descriptor (b/27403984).
multicastSocket.close();
- multicastNetworkInterfaceProvider.stopWatchingConnectivityChanges();
}
/**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
index 82c8c5b..9cfcba1 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -25,6 +25,7 @@
import android.net.wifi.WifiManager.MulticastLock;
import android.os.SystemClock;
import android.text.format.DateUtils;
+import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.SharedLog;
@@ -106,6 +107,7 @@
@Nullable private Timer checkMulticastResponseTimer;
private final SharedLog sharedLog;
@NonNull private final MdnsFeatureFlags mdnsFeatureFlags;
+ private final MulticastNetworkInterfaceProvider interfaceProvider;
public MdnsSocketClient(@NonNull Context context, @NonNull MulticastLock multicastLock,
SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
@@ -118,6 +120,7 @@
unicastReceiverBuffer = null;
}
this.mdnsFeatureFlags = mdnsFeatureFlags;
+ this.interfaceProvider = new MulticastNetworkInterfaceProvider(context, sharedLog);
}
@Override
@@ -138,6 +141,7 @@
cannotReceiveMulticastResponse.set(false);
shouldStopSocketLoop = false;
+ interfaceProvider.startWatchingConnectivityChanges();
try {
// TODO (changed when importing code): consider setting thread stats tag
multicastSocket = createMdnsSocket(MdnsConstants.MDNS_PORT, sharedLog);
@@ -183,6 +187,7 @@
}
multicastLock.release();
+ interfaceProvider.stopWatchingConnectivityChanges();
shouldStopSocketLoop = true;
waitForSendThreadToStop();
@@ -202,18 +207,18 @@
}
@Override
- public void sendPacketRequestingMulticastResponse(@NonNull DatagramPacket packet,
+ public void sendPacketRequestingMulticastResponse(@NonNull List<DatagramPacket> packets,
boolean onlyUseIpv6OnIpv6OnlyNetworks) {
- sendMdnsPacket(packet, multicastPacketQueue, onlyUseIpv6OnIpv6OnlyNetworks);
+ sendMdnsPackets(packets, multicastPacketQueue, onlyUseIpv6OnIpv6OnlyNetworks);
}
@Override
- public void sendPacketRequestingUnicastResponse(@NonNull DatagramPacket packet,
+ public void sendPacketRequestingUnicastResponse(@NonNull List<DatagramPacket> packets,
boolean onlyUseIpv6OnIpv6OnlyNetworks) {
if (useSeparateSocketForUnicast) {
- sendMdnsPacket(packet, unicastPacketQueue, onlyUseIpv6OnIpv6OnlyNetworks);
+ sendMdnsPackets(packets, unicastPacketQueue, onlyUseIpv6OnIpv6OnlyNetworks);
} else {
- sendMdnsPacket(packet, multicastPacketQueue, onlyUseIpv6OnIpv6OnlyNetworks);
+ sendMdnsPackets(packets, multicastPacketQueue, onlyUseIpv6OnIpv6OnlyNetworks);
}
}
@@ -234,17 +239,21 @@
return false;
}
- private void sendMdnsPacket(DatagramPacket packet, Queue<DatagramPacket> packetQueueToUse,
- boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+ private void sendMdnsPackets(List<DatagramPacket> packets,
+ Queue<DatagramPacket> packetQueueToUse, boolean onlyUseIpv6OnIpv6OnlyNetworks) {
if (shouldStopSocketLoop && !MdnsConfigs.allowAddMdnsPacketAfterDiscoveryStops()) {
sharedLog.w("sendMdnsPacket() is called after discovery already stopped");
return;
}
+ if (packets.isEmpty()) {
+ Log.wtf(TAG, "No mDns packets to send");
+ return;
+ }
- final boolean isIpv4 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
- instanceof Inet4Address;
- final boolean isIpv6 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
- instanceof Inet6Address;
+ final boolean isIpv4 = ((InetSocketAddress) packets.get(0).getSocketAddress())
+ .getAddress() instanceof Inet4Address;
+ final boolean isIpv6 = ((InetSocketAddress) packets.get(0).getSocketAddress())
+ .getAddress() instanceof Inet6Address;
final boolean ipv6Only = multicastSocket != null && multicastSocket.isOnIPv6OnlyNetwork();
if (isIpv4 && ipv6Only) {
return;
@@ -254,10 +263,11 @@
}
synchronized (packetQueueToUse) {
- while (packetQueueToUse.size() >= MdnsConfigs.mdnsPacketQueueMaxSize()) {
+ while ((packetQueueToUse.size() + packets.size())
+ > MdnsConfigs.mdnsPacketQueueMaxSize()) {
packetQueueToUse.remove();
}
- packetQueueToUse.add(packet);
+ packetQueueToUse.addAll(packets);
}
triggerSendThread();
}
@@ -482,8 +492,7 @@
@VisibleForTesting
MdnsSocket createMdnsSocket(int port, SharedLog sharedLog) throws IOException {
- return new MdnsSocket(new MulticastNetworkInterfaceProvider(context, sharedLog), port,
- sharedLog);
+ return new MdnsSocket(interfaceProvider, port, sharedLog);
}
private void sendPackets(List<DatagramPacket> packets, MdnsSocket socket) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
index b6000f0..b1a543a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
@@ -23,6 +23,7 @@
import java.io.IOException;
import java.net.DatagramPacket;
+import java.util.List;
/**
* Base class for multicast socket client.
@@ -40,15 +41,15 @@
void setCallback(@Nullable Callback callback);
/**
- * Send a mDNS request packet via given network that asks for multicast response.
+ * Send mDNS request packets via given network that asks for multicast response.
*/
- void sendPacketRequestingMulticastResponse(@NonNull DatagramPacket packet,
+ void sendPacketRequestingMulticastResponse(@NonNull List<DatagramPacket> packets,
boolean onlyUseIpv6OnIpv6OnlyNetworks);
/**
- * Send a mDNS request packet via given network that asks for unicast response.
+ * Send mDNS request packets via given network that asks for unicast response.
*/
- void sendPacketRequestingUnicastResponse(@NonNull DatagramPacket packet,
+ void sendPacketRequestingUnicastResponse(@NonNull List<DatagramPacket> packets,
boolean onlyUseIpv6OnIpv6OnlyNetworks);
/*** Notify that the given network is requested for mdns discovery / resolution */
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/connectivity/mdns/QueryTaskConfig.java b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
index 19282b0..0894166 100644
--- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -16,15 +16,14 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
/**
* A configuration for the PeriodicalQueryTask that contains parameters to build a query packet.
* Call to getConfigForNextRun returns a config that can be used to build the next query task.
@@ -33,19 +32,26 @@
private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
(int) MdnsConfigs.initialTimeBetweenBurstsMs();
- private static final int TIME_BETWEEN_BURSTS_MS = (int) MdnsConfigs.timeBetweenBurstsMs();
+ private static final int MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS =
+ (int) MdnsConfigs.timeBetweenBurstsMs();
private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
(int) MdnsConfigs.timeBetweenQueriesInBurstMs();
private static final int QUERIES_PER_BURST_PASSIVE_MODE =
(int) MdnsConfigs.queriesPerBurstPassive();
private static final int UNSIGNED_SHORT_MAX_VALUE = 65536;
- // The following fields are used by QueryTask so we need to test them.
@VisibleForTesting
- final List<String> subtypes;
+ // RFC 6762 5.2: The interval between the first two queries MUST be at least one second.
+ static final int INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS = 1000;
+ @VisibleForTesting
+ // Basically this tries to send one query per typical DTIM interval 100ms, to maximize the
+ // chances that a query will be received if devices are using a DTIM multiplier (in which case
+ // they only listen once every [multiplier] DTIM intervals).
+ static final int TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS = 100;
+ static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000;
private final boolean alwaysAskForUnicastResponse =
MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
- private final boolean usePassiveMode;
+ private final int queryMode;
final boolean onlyUseIpv6OnIpv6OnlyNetworks;
private final int numOfQueriesBeforeBackoff;
@VisibleForTesting
@@ -65,8 +71,7 @@
boolean expectUnicastResponse, boolean isFirstBurst, int burstCounter,
int queriesPerBurst, int timeBetweenBurstsInMs,
long delayUntilNextTaskWithoutBackoffMs) {
- this.subtypes = new ArrayList<>(other.subtypes);
- this.usePassiveMode = other.usePassiveMode;
+ this.queryMode = other.queryMode;
this.onlyUseIpv6OnIpv6OnlyNetworks = other.onlyUseIpv6OnIpv6OnlyNetworks;
this.numOfQueriesBeforeBackoff = other.numOfQueriesBeforeBackoff;
this.transactionId = transactionId;
@@ -79,36 +84,72 @@
this.queryCount = queryCount;
this.socketKey = other.socketKey;
}
- QueryTaskConfig(@NonNull Collection<String> subtypes,
- boolean usePassiveMode,
+
+ QueryTaskConfig(int queryMode,
boolean onlyUseIpv6OnIpv6OnlyNetworks,
int numOfQueriesBeforeBackoff,
@Nullable SocketKey socketKey) {
- this.usePassiveMode = usePassiveMode;
+ this.queryMode = queryMode;
this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
- this.subtypes = new ArrayList<>(subtypes);
this.queriesPerBurst = QUERIES_PER_BURST;
this.burstCounter = 0;
this.transactionId = 1;
this.expectUnicastResponse = true;
this.isFirstBurst = true;
// Config the scan frequency based on the scan mode.
- if (this.usePassiveMode) {
+ if (this.queryMode == AGGRESSIVE_QUERY_MODE) {
+ this.timeBetweenBurstsInMs = INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
+ this.delayUntilNextTaskWithoutBackoffMs =
+ TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
+ } else if (this.queryMode == PASSIVE_QUERY_MODE) {
// In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then
// in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
// queries.
- this.timeBetweenBurstsInMs = TIME_BETWEEN_BURSTS_MS;
+ this.timeBetweenBurstsInMs = MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
+ this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
} else {
// In active scan mode, sends a burst of QUERIES_PER_BURST queries,
// TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
// then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
// doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
+ this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
}
this.socketKey = socketKey;
this.queryCount = 0;
- this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
+ }
+
+ long getDelayUntilNextTaskWithoutBackoff(boolean isFirstQueryInBurst,
+ boolean isLastQueryInBurst) {
+ if (isFirstQueryInBurst && queryMode == AGGRESSIVE_QUERY_MODE) {
+ return 0;
+ }
+ if (isLastQueryInBurst) {
+ return timeBetweenBurstsInMs;
+ }
+ return queryMode == AGGRESSIVE_QUERY_MODE
+ ? TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS
+ : TIME_BETWEEN_QUERIES_IN_BURST_MS;
+ }
+
+ boolean getNextExpectUnicastResponse(boolean isLastQueryInBurst) {
+ if (!isLastQueryInBurst) {
+ return false;
+ }
+ if (queryMode == AGGRESSIVE_QUERY_MODE) {
+ return true;
+ }
+ return alwaysAskForUnicastResponse;
+ }
+
+ int getNextTimeBetweenBurstsMs(boolean isLastQueryInBurst) {
+ if (!isLastQueryInBurst) {
+ return timeBetweenBurstsInMs;
+ }
+ final int maxTimeBetweenBursts = queryMode == AGGRESSIVE_QUERY_MODE
+ ? MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS : MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
+ return Math.min(timeBetweenBurstsInMs * 2, maxTimeBetweenBursts);
}
/**
@@ -120,43 +161,26 @@
if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) {
newTransactionId = 1;
}
- boolean newExpectUnicastResponse = false;
- boolean newIsFirstBurst = isFirstBurst;
+
int newQueriesPerBurst = queriesPerBurst;
int newBurstCounter = burstCounter + 1;
- long newDelayUntilNextTaskWithoutBackoffMs = delayUntilNextTaskWithoutBackoffMs;
- int newTimeBetweenBurstsInMs = timeBetweenBurstsInMs;
- // Only the first query expects uni-cast response.
- if (newBurstCounter == queriesPerBurst) {
+ final boolean isFirstQueryInBurst = newBurstCounter == 1;
+ final boolean isLastQueryInBurst = newBurstCounter == queriesPerBurst;
+ boolean newIsFirstBurst = isFirstBurst && !isLastQueryInBurst;
+ if (isLastQueryInBurst) {
newBurstCounter = 0;
-
- if (alwaysAskForUnicastResponse) {
- newExpectUnicastResponse = true;
- }
// In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and
// then in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
// queries.
- if (isFirstBurst) {
- newIsFirstBurst = false;
- if (usePassiveMode) {
- newQueriesPerBurst = QUERIES_PER_BURST_PASSIVE_MODE;
- }
+ if (isFirstBurst && queryMode == PASSIVE_QUERY_MODE) {
+ newQueriesPerBurst = QUERIES_PER_BURST_PASSIVE_MODE;
}
- // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
- // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
- // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
- // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
- newDelayUntilNextTaskWithoutBackoffMs = timeBetweenBurstsInMs;
- if (timeBetweenBurstsInMs < TIME_BETWEEN_BURSTS_MS) {
- newTimeBetweenBurstsInMs = Math.min(timeBetweenBurstsInMs * 2,
- TIME_BETWEEN_BURSTS_MS);
- }
- } else {
- newDelayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
}
+
return new QueryTaskConfig(this, newQueryCount, newTransactionId,
- newExpectUnicastResponse, newIsFirstBurst, newBurstCounter, newQueriesPerBurst,
- newTimeBetweenBurstsInMs, newDelayUntilNextTaskWithoutBackoffMs);
+ getNextExpectUnicastResponse(isLastQueryInBurst), newIsFirstBurst, newBurstCounter,
+ newQueriesPerBurst, getNextTimeBetweenBurstsMs(isLastQueryInBurst),
+ getDelayUntilNextTaskWithoutBackoff(isFirstQueryInBurst, isLastQueryInBurst));
}
/**
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index 8fc8114..3c11a24 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -16,6 +16,8 @@
package com.android.server.connectivity.mdns.util;
+import static com.android.server.connectivity.mdns.MdnsConstants.FLAG_TRUNCATED;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.Network;
@@ -23,6 +25,7 @@
import android.os.Handler;
import android.os.SystemClock;
import android.util.ArraySet;
+import android.util.Pair;
import com.android.server.connectivity.mdns.MdnsConstants;
import com.android.server.connectivity.mdns.MdnsPacket;
@@ -30,13 +33,18 @@
import com.android.server.connectivity.mdns.MdnsRecord;
import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
/**
@@ -86,7 +94,10 @@
/**
* Compare two strings by DNS case-insensitive lowercase.
*/
- public static boolean equalsIgnoreDnsCase(@NonNull String a, @NonNull String b) {
+ public static boolean equalsIgnoreDnsCase(@Nullable String a, @Nullable String b) {
+ if (a == null || b == null) {
+ return a == null && b == null;
+ }
if (a.length() != b.length()) return false;
for (int i = 0; i < a.length(); i++) {
if (toDnsLowerCase(a.charAt(i)) != toDnsLowerCase(b.charAt(i))) {
@@ -223,6 +234,100 @@
}
/**
+ * Writes the possible query content of an MdnsPacket into the data buffer.
+ *
+ * <p>This method is specifically for query packets. It writes the question and answer sections
+ * into the data buffer only.
+ *
+ * @param packetCreationBuffer The data buffer for the query content.
+ * @param packet The MdnsPacket to be written into the data buffer.
+ * @return A Pair containing:
+ * 1. The remaining MdnsPacket data that could not fit in the buffer.
+ * 2. The length of the data written to the buffer.
+ */
+ @Nullable
+ private static Pair<MdnsPacket, Integer> writePossibleMdnsPacket(
+ @NonNull byte[] packetCreationBuffer, @NonNull MdnsPacket packet) throws IOException {
+ MdnsPacket remainingPacket;
+ final MdnsPacketWriter writer = new MdnsPacketWriter(packetCreationBuffer);
+ writer.writeUInt16(packet.transactionId); // Transaction ID
+
+ final int flagsPos = writer.getWritePosition();
+ writer.writeUInt16(0); // Flags, written later
+ writer.writeUInt16(0); // questions count, written later
+ writer.writeUInt16(0); // answers count, written later
+ writer.writeUInt16(0); // authority entries count, empty session for query
+ writer.writeUInt16(0); // additional records count, empty session for query
+
+ int writtenQuestions = 0;
+ int writtenAnswers = 0;
+ int lastValidPos = writer.getWritePosition();
+ try {
+ for (MdnsRecord record : packet.questions) {
+ // Questions do not have TTL or data
+ record.writeHeaderFields(writer);
+ writtenQuestions++;
+ lastValidPos = writer.getWritePosition();
+ }
+ for (MdnsRecord record : packet.answers) {
+ record.write(writer, 0L);
+ writtenAnswers++;
+ lastValidPos = writer.getWritePosition();
+ }
+ remainingPacket = null;
+ } catch (IOException e) {
+ // Went over the packet limit; truncate
+ if (writtenQuestions == 0 && writtenAnswers == 0) {
+ // No space to write even one record: just throw (as subclass of IOException)
+ throw e;
+ }
+
+ // Set the last valid position as the final position (not as a rewind)
+ writer.rewind(lastValidPos);
+ writer.clearRewind();
+
+ remainingPacket = new MdnsPacket(packet.flags,
+ packet.questions.subList(
+ writtenQuestions, packet.questions.size()),
+ packet.answers.subList(
+ writtenAnswers, packet.answers.size()),
+ Collections.emptyList(), /* authorityRecords */
+ Collections.emptyList() /* additionalRecords */);
+ }
+
+ final int len = writer.getWritePosition();
+ writer.rewind(flagsPos);
+ writer.writeUInt16(packet.flags | (remainingPacket == null ? 0 : FLAG_TRUNCATED));
+ writer.writeUInt16(writtenQuestions);
+ writer.writeUInt16(writtenAnswers);
+ writer.unrewind();
+
+ return Pair.create(remainingPacket, len);
+ }
+
+ /**
+ * Create Datagram packets from given MdnsPacket and InetSocketAddress.
+ *
+ * <p> If the MdnsPacket is too large for a single DatagramPacket, it will be split into
+ * multiple DatagramPackets.
+ */
+ public static List<DatagramPacket> createQueryDatagramPackets(
+ @NonNull byte[] packetCreationBuffer, @NonNull MdnsPacket packet,
+ @NonNull InetSocketAddress destination) throws IOException {
+ final List<DatagramPacket> datagramPackets = new ArrayList<>();
+ MdnsPacket remainingPacket = packet;
+ while (remainingPacket != null) {
+ final Pair<MdnsPacket, Integer> result =
+ writePossibleMdnsPacket(packetCreationBuffer, remainingPacket);
+ remainingPacket = result.first;
+ final int len = result.second;
+ final byte[] outBuffer = Arrays.copyOfRange(packetCreationBuffer, 0, len);
+ datagramPackets.add(new DatagramPacket(outBuffer, 0, outBuffer.length, destination));
+ }
+ return datagramPackets;
+ }
+
+ /**
* Checks if the MdnsRecord needs to be renewed or not.
*
* <p>As per RFC6762 7.1 no need to query if remaining TTL is more than half the original one,
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index 0b54fdd..cadc04d 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -250,6 +250,17 @@
return mTrackingInterfaces.containsKey(ifaceName);
}
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ @Nullable
+ protected String getHwAddress(@NonNull final String ifaceName) {
+ if (!hasInterface(ifaceName)) {
+ return null;
+ }
+
+ NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
+ return iface.mHwAddress;
+ }
+
@VisibleForTesting
static class NetworkInterfaceState {
final String name;
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index e7af569..b8689d6 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -19,6 +19,7 @@
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import android.annotation.CheckResult;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -72,8 +73,9 @@
methodName + " is only available on automotive devices.");
}
- private boolean checkUseRestrictedNetworksPermission() {
- return PermissionUtils.checkAnyPermissionOf(mContext,
+ @CheckResult
+ private boolean hasUseRestrictedNetworksPermission() {
+ return PermissionUtils.hasAnyPermissionOf(mContext,
android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS);
}
@@ -92,7 +94,7 @@
@Override
public String[] getAvailableInterfaces() throws RemoteException {
PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
- return mTracker.getClientModeInterfaces(checkUseRestrictedNetworksPermission());
+ return mTracker.getClientModeInterfaces(hasUseRestrictedNetworksPermission());
}
/**
@@ -146,7 +148,7 @@
public void addListener(IEthernetServiceListener listener) throws RemoteException {
Objects.requireNonNull(listener, "listener must not be null");
PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
- mTracker.addListener(listener, checkUseRestrictedNetworksPermission());
+ mTracker.addListener(listener, hasUseRestrictedNetworksPermission());
}
/**
@@ -187,7 +189,7 @@
@Override
protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
- if (!PermissionUtils.checkDumpPermission(mContext, TAG, pw)) return;
+ if (!PermissionUtils.hasDumpPermission(mContext, TAG, pw)) return;
pw.println("Current Ethernet state: ");
pw.increaseIndent();
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 458d64f..71f289e 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -131,6 +131,10 @@
// returned when a tethered interface is requested; until then, it remains in client mode. Its
// current mode is reflected in mTetheringInterfaceMode.
private String mTetheringInterface;
+ // If the tethering interface is in server mode, it is not tracked by factory. The HW address
+ // must be maintained by the EthernetTracker. Its current mode is reflected in
+ // mTetheringInterfaceMode.
+ private String mTetheringInterfaceHwAddr;
private int mTetheringInterfaceMode = INTERFACE_MODE_CLIENT;
// Tracks whether clients were notified that the tethered interface is available
private boolean mTetheredInterfaceWasAvailable = false;
@@ -382,10 +386,9 @@
});
}
- @VisibleForTesting(visibility = PACKAGE)
- protected void setInterfaceEnabled(@NonNull final String iface, boolean enabled,
- @Nullable final EthernetCallback cb) {
- mHandler.post(() -> updateInterfaceState(iface, enabled, cb));
+ /** Configure the administrative state of ethernet interface by toggling IFF_UP. */
+ public void setInterfaceEnabled(String iface, boolean enabled, EthernetCallback cb) {
+ mHandler.post(() -> setInterfaceAdministrativeState(iface, enabled, cb));
}
IpConfiguration getIpConfiguration(String iface) {
@@ -461,7 +464,7 @@
if (!include) {
removeTestData();
}
- mHandler.post(() -> trackAvailableInterfaces());
+ trackAvailableInterfaces();
});
}
@@ -583,6 +586,7 @@
removeInterface(iface);
if (iface.equals(mTetheringInterface)) {
mTetheringInterface = null;
+ mTetheringInterfaceHwAddr = null;
}
broadcastInterfaceStateChange(iface);
}
@@ -611,13 +615,14 @@
return;
}
+ final String hwAddress = config.hwAddr;
+
if (getInterfaceMode(iface) == INTERFACE_MODE_SERVER) {
maybeUpdateServerModeInterfaceState(iface, true);
+ mTetheringInterfaceHwAddr = hwAddress;
return;
}
- final String hwAddress = config.hwAddr;
-
NetworkCapabilities nc = mNetworkCapabilities.get(iface);
if (nc == null) {
// Try to resolve using mac address
@@ -643,25 +648,40 @@
}
}
- private void updateInterfaceState(String iface, boolean up) {
- updateInterfaceState(iface, up, new EthernetCallback(null /* cb */));
- }
-
- // TODO(b/225315248): enable/disableInterface() should not affect link state.
- private void updateInterfaceState(String iface, boolean up, EthernetCallback cb) {
- final int mode = getInterfaceMode(iface);
- if (mode == INTERFACE_MODE_SERVER || !mFactory.hasInterface(iface)) {
- // The interface is in server mode or is not tracked.
- cb.onError("Failed to set link state " + (up ? "up" : "down") + " for " + iface);
+ private void setInterfaceAdministrativeState(String iface, boolean up, EthernetCallback cb) {
+ if (getInterfaceState(iface) == EthernetManager.STATE_ABSENT) {
+ cb.onError("Failed to enable/disable absent interface: " + iface);
+ return;
+ }
+ if (getInterfaceRole(iface) == EthernetManager.ROLE_SERVER) {
+ // TODO: support setEthernetState for server mode interfaces.
+ cb.onError("Failed to enable/disable interface in server mode: " + iface);
return;
}
+ if (up) {
+ // WARNING! setInterfaceUp() clears the IPv4 address and readds it. Calling
+ // enableInterface() on an active interface can lead to a provisioning failure which
+ // will cause IpClient to be restarted.
+ // TODO: use netlink directly rather than calling into netd.
+ NetdUtils.setInterfaceUp(mNetd, iface);
+ } else {
+ NetdUtils.setInterfaceDown(mNetd, iface);
+ }
+ cb.onResult(iface);
+ }
+
+ private void updateInterfaceState(String iface, boolean up) {
+ final int mode = getInterfaceMode(iface);
+ if (mode == INTERFACE_MODE_SERVER) {
+ // TODO: support tracking link state for interfaces in server mode.
+ return;
+ }
+
+ // If updateInterfaceLinkState returns false, the interface is already in the correct state.
if (mFactory.updateInterfaceLinkState(iface, up)) {
broadcastInterfaceStateChange(iface);
}
- // If updateInterfaceLinkState returns false, the interface is already in the correct state.
- // Always return success.
- cb.onResult(iface);
}
private void maybeUpdateServerModeInterfaceState(String iface, boolean available) {
diff --git a/service-t/src/com/android/server/net/BpfInterfaceMapHelper.java b/service-t/src/com/android/server/net/BpfInterfaceMapHelper.java
new file mode 100644
index 0000000..3c95b8e
--- /dev/null
+++ b/service-t/src/com/android/server/net/BpfInterfaceMapHelper.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net;
+
+import android.os.Build;
+import android.system.ErrnoException;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.BpfDump;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct.S32;
+
+/**
+ * Monitor interface added (without removed) and right interface name and its index to bpf map.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+public class BpfInterfaceMapHelper {
+ private static final String TAG = BpfInterfaceMapHelper.class.getSimpleName();
+ // This is current path but may be changed soon.
+ private static final String IFACE_INDEX_NAME_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_iface_index_name_map";
+ private final IBpfMap<S32, InterfaceMapValue> mIndexToIfaceBpfMap;
+
+ public BpfInterfaceMapHelper() {
+ this(new Dependencies());
+ }
+
+ @VisibleForTesting
+ public BpfInterfaceMapHelper(Dependencies deps) {
+ mIndexToIfaceBpfMap = deps.getInterfaceMap();
+ }
+
+ /**
+ * Dependencies of BpfInerfaceMapUpdater, for injection in tests.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ /** Create BpfMap for updating interface and index mapping. */
+ public IBpfMap<S32, InterfaceMapValue> getInterfaceMap() {
+ try {
+ return new BpfMap<>(IFACE_INDEX_NAME_MAP_PATH,
+ S32.class, InterfaceMapValue.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create interface map: " + e);
+ return null;
+ }
+ }
+ }
+
+ /** get interface name by interface index from bpf map */
+ public String getIfNameByIndex(final int index) {
+ try {
+ final InterfaceMapValue value = mIndexToIfaceBpfMap.getValue(new S32(index));
+ if (value == null) {
+ Log.e(TAG, "No if name entry for index " + index);
+ return null;
+ }
+ return value.getInterfaceNameString();
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to get entry for index " + index + ": " + e);
+ return null;
+ }
+ }
+
+ /**
+ * Dump BPF map
+ *
+ * @param pw print writer
+ */
+ public void dump(final IndentingPrintWriter pw) {
+ pw.println("BPF map status:");
+ pw.increaseIndent();
+ BpfDump.dumpMapStatus(mIndexToIfaceBpfMap, pw, "IfaceIndexNameMap",
+ IFACE_INDEX_NAME_MAP_PATH);
+ pw.decreaseIndent();
+ pw.println("BPF map content:");
+ pw.increaseIndent();
+ BpfDump.dumpMap(mIndexToIfaceBpfMap, pw, "IfaceIndexNameMap",
+ (key, value) -> "ifaceIndex=" + key.val
+ + " ifaceName=" + value.getInterfaceNameString());
+ pw.decreaseIndent();
+ }
+}
diff --git a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
deleted file mode 100644
index 27c0f9f..0000000
--- a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.server.net;
-
-import android.content.Context;
-import android.net.INetd;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.os.ServiceSpecificException;
-import android.system.ErrnoException;
-import android.util.IndentingPrintWriter;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
-import com.android.net.module.util.BpfDump;
-import com.android.net.module.util.BpfMap;
-import com.android.net.module.util.IBpfMap;
-import com.android.net.module.util.InterfaceParams;
-import com.android.net.module.util.Struct.S32;
-
-/**
- * Monitor interface added (without removed) and right interface name and its index to bpf map.
- */
-public class BpfInterfaceMapUpdater {
- private static final String TAG = BpfInterfaceMapUpdater.class.getSimpleName();
- // This is current path but may be changed soon.
- private static final String IFACE_INDEX_NAME_MAP_PATH =
- "/sys/fs/bpf/netd_shared/map_netd_iface_index_name_map";
- private final IBpfMap<S32, InterfaceMapValue> mIndexToIfaceBpfMap;
- private final INetd mNetd;
- private final Handler mHandler;
- private final Dependencies mDeps;
-
- public BpfInterfaceMapUpdater(Context ctx, Handler handler) {
- this(ctx, handler, new Dependencies());
- }
-
- @VisibleForTesting
- public BpfInterfaceMapUpdater(Context ctx, Handler handler, Dependencies deps) {
- mDeps = deps;
- mIndexToIfaceBpfMap = deps.getInterfaceMap();
- mNetd = deps.getINetd(ctx);
- mHandler = handler;
- }
-
- /**
- * Dependencies of BpfInerfaceMapUpdater, for injection in tests.
- */
- @VisibleForTesting
- public static class Dependencies {
- /** 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,
- S32.class, InterfaceMapValue.class);
- } catch (ErrnoException e) {
- Log.e(TAG, "Cannot create interface map: " + e);
- return null;
- }
- }
-
- /** Get InterfaceParams for giving interface name. */
- public InterfaceParams getInterfaceParams(String ifaceName) {
- return InterfaceParams.getByName(ifaceName);
- }
-
- /** Get INetd binder object. */
- public INetd getINetd(Context ctx) {
- return INetd.Stub.asInterface((IBinder) ctx.getSystemService(Context.NETD_SERVICE));
- }
- }
-
- /**
- * Start listening interface update event.
- * Query current interface names before listening.
- */
- public void start() {
- mHandler.post(() -> {
- if (mIndexToIfaceBpfMap == null) {
- Log.wtf(TAG, "Fail to start: Null bpf map");
- return;
- }
-
- try {
- // TODO: use a NetlinkMonitor and listen for RTM_NEWLINK messages instead.
- mNetd.registerUnsolicitedEventListener(new InterfaceChangeObserver());
- } catch (RemoteException e) {
- Log.wtf(TAG, "Unable to register netd UnsolicitedEventListener, " + e);
- }
-
- final String[] ifaces;
- try {
- // TODO: use a netlink dump to get the current interface list.
- ifaces = mNetd.interfaceGetList();
- } catch (RemoteException | ServiceSpecificException e) {
- Log.wtf(TAG, "Unable to query interface names by netd, " + e);
- return;
- }
-
- for (String ifaceName : ifaces) {
- addInterface(ifaceName);
- }
- });
- }
-
- private void addInterface(String ifaceName) {
- final InterfaceParams iface = mDeps.getInterfaceParams(ifaceName);
- if (iface == null) {
- Log.e(TAG, "Unable to get InterfaceParams for " + ifaceName);
- return;
- }
-
- try {
- mIndexToIfaceBpfMap.updateEntry(new S32(iface.index), new InterfaceMapValue(ifaceName));
- } catch (ErrnoException e) {
- Log.e(TAG, "Unable to update entry for " + ifaceName + ", " + e);
- }
- }
-
- private class InterfaceChangeObserver extends BaseNetdUnsolicitedEventListener {
- @Override
- public void onInterfaceAdded(String ifName) {
- mHandler.post(() -> addInterface(ifName));
- }
- }
-
- /** get interface name by interface index from bpf map */
- public String getIfNameByIndex(final int index) {
- try {
- final InterfaceMapValue value = mIndexToIfaceBpfMap.getValue(new S32(index));
- if (value == null) {
- Log.e(TAG, "No if name entry for index " + index);
- return null;
- }
- return value.getInterfaceNameString();
- } catch (ErrnoException e) {
- Log.e(TAG, "Failed to get entry for index " + index + ": " + e);
- return null;
- }
- }
-
- /**
- * Dump BPF map
- *
- * @param pw print writer
- */
- public void dump(final IndentingPrintWriter pw) {
- pw.println("BPF map status:");
- pw.increaseIndent();
- BpfDump.dumpMapStatus(mIndexToIfaceBpfMap, pw, "IfaceIndexNameMap",
- IFACE_INDEX_NAME_MAP_PATH);
- pw.decreaseIndent();
- pw.println("BPF map content:");
- pw.increaseIndent();
- BpfDump.dumpMap(mIndexToIfaceBpfMap, pw, "IfaceIndexNameMap",
- (key, value) -> "ifaceIndex=" + key.val
- + " ifaceName=" + value.getInterfaceNameString());
- pw.decreaseIndent();
- }
-}
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index eb75461..4438b87 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -476,11 +476,13 @@
private final LocationPermissionChecker mLocationPermissionChecker;
@NonNull
- private final BpfInterfaceMapUpdater mInterfaceMapUpdater;
+ private final BpfInterfaceMapHelper mInterfaceMapHelper;
@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());
@@ -492,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;
}
@@ -625,8 +628,7 @@
mContentObserver = mDeps.makeContentObserver(mHandler, mSettings,
mNetworkStatsSubscriptionsMonitor);
mLocationPermissionChecker = mDeps.makeLocationPermissionChecker(mContext);
- mInterfaceMapUpdater = mDeps.makeBpfInterfaceMapUpdater(mContext, mHandler);
- mInterfaceMapUpdater.start();
+ mInterfaceMapHelper = mDeps.makeBpfInterfaceMapHelper();
mUidCounterSetMap = mDeps.getUidCounterSetMap();
mCookieTagMap = mDeps.getCookieTagMap();
mStatsMapA = mDeps.getStatsMapA();
@@ -795,18 +797,16 @@
return new LocationPermissionChecker(context);
}
- /** Create BpfInterfaceMapUpdater to update bpf interface map. */
+ /** Create BpfInterfaceMapHelper to update bpf interface map. */
@NonNull
- public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
- @NonNull Context ctx, @NonNull Handler handler) {
- return new BpfInterfaceMapUpdater(ctx, handler);
+ public BpfInterfaceMapHelper makeBpfInterfaceMapHelper() {
+ return new BpfInterfaceMapHelper();
}
/** 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,
- S32.class, U8.class);
+ return new BpfMap<>(UID_COUNTERSET_MAP_PATH, S32.class, U8.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open uid counter set map: " + e);
return null;
@@ -816,8 +816,8 @@
/** Gets the cookie tag map */
public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
try {
- return new BpfMap<CookieTagMapKey, CookieTagMapValue>(COOKIE_TAG_MAP_PATH,
- BpfMap.BPF_F_RDWR, CookieTagMapKey.class, CookieTagMapValue.class);
+ return new BpfMap<>(COOKIE_TAG_MAP_PATH,
+ CookieTagMapKey.class, CookieTagMapValue.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open cookie tag map: " + e);
return null;
@@ -827,8 +827,7 @@
/** Gets stats map A */
public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
try {
- return new BpfMap<StatsMapKey, StatsMapValue>(STATS_MAP_A_PATH,
- BpfMap.BPF_F_RDWR, StatsMapKey.class, StatsMapValue.class);
+ return new BpfMap<>(STATS_MAP_A_PATH, StatsMapKey.class, StatsMapValue.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open stats map A: " + e);
return null;
@@ -838,8 +837,7 @@
/** Gets stats map B */
public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
try {
- return new BpfMap<StatsMapKey, StatsMapValue>(STATS_MAP_B_PATH,
- BpfMap.BPF_F_RDWR, StatsMapKey.class, StatsMapValue.class);
+ return new BpfMap<>(STATS_MAP_B_PATH, StatsMapKey.class, StatsMapValue.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open stats map B: " + e);
return null;
@@ -849,8 +847,8 @@
/** Gets the uid stats map */
public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
try {
- return new BpfMap<UidStatsMapKey, StatsMapValue>(APP_UID_STATS_MAP_PATH,
- BpfMap.BPF_F_RDWR, UidStatsMapKey.class, StatsMapValue.class);
+ return new BpfMap<>(APP_UID_STATS_MAP_PATH,
+ UidStatsMapKey.class, StatsMapValue.class);
} catch (ErrnoException e) {
Log.wtf(TAG, "Cannot open app uid stats map: " + e);
return null;
@@ -860,8 +858,7 @@
/** Gets interface stats map */
public IBpfMap<S32, StatsMapValue> getIfaceStatsMap() {
try {
- return new BpfMap<S32, StatsMapValue>(IFACE_STATS_MAP_PATH,
- BpfMap.BPF_F_RDWR, S32.class, StatsMapValue.class);
+ return new BpfMap<>(IFACE_STATS_MAP_PATH, S32.class, StatsMapValue.class);
} catch (ErrnoException e) {
throw new IllegalStateException("Failed to open interface stats map", e);
}
@@ -880,7 +877,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));
}
/**
@@ -1432,7 +1430,11 @@
}
@Override
- public INetworkStatsSession openSessionForUsageStats(int flags, String callingPackage) {
+ public INetworkStatsSession openSessionForUsageStats(
+ int flags, @NonNull String callingPackage) {
+ Objects.requireNonNull(callingPackage);
+ PermissionUtils.enforcePackageNameMatchesUid(
+ mContext, Binder.getCallingUid(), callingPackage);
return openSessionInternal(flags, callingPackage);
}
@@ -1461,9 +1463,9 @@
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,
+ final boolean isPrivileged = PermissionUtils.hasAnyPermissionOf(mContext,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
android.Manifest.permission.NETWORK_STACK);
if (!isPrivileged) {
@@ -1478,7 +1480,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) {
@@ -1495,6 +1498,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);
@@ -1633,7 +1637,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.
@@ -1654,7 +1658,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);
}
@@ -1779,6 +1783,8 @@
if (transport == TRANSPORT_WIFI) {
ifaceSet = mAllWifiIfacesSinceBoot;
} else if (transport == TRANSPORT_CELLULAR) {
+ // Since satellite networks appear under type mobile, this includes both cellular
+ // and satellite active interfaces
ifaceSet = mAllMobileIfacesSinceBoot;
} else {
throw new IllegalArgumentException("Invalid transport " + transport);
@@ -1945,6 +1951,7 @@
final int callingPid = Binder.getCallingPid();
final int callingUid = Binder.getCallingUid();
+ PermissionUtils.enforcePackageNameMatchesUid(mContext, callingUid, callingPackage);
@NetworkStatsAccess.Level int accessLevel = checkAccessLevel(callingPackage);
DataUsageRequest normalizedRequest;
final long token = Binder.clearCallingIdentity();
@@ -2188,7 +2195,9 @@
for (NetworkStateSnapshot snapshot : snapshots) {
final int displayTransport =
getDisplayTransport(snapshot.getNetworkCapabilities().getTransportTypes());
- final boolean isMobile = (NetworkCapabilities.TRANSPORT_CELLULAR == displayTransport);
+ // Consider satellite transport to support satellite stats appear as type_mobile
+ final boolean isMobile = NetworkCapabilities.TRANSPORT_CELLULAR == displayTransport
+ || NetworkCapabilities.TRANSPORT_SATELLITE == displayTransport;
final boolean isWifi = (NetworkCapabilities.TRANSPORT_WIFI == displayTransport);
final boolean isDefault = CollectionUtils.contains(
mDefaultNetworks, snapshot.getNetwork());
@@ -2209,6 +2218,7 @@
// both total usage and UID details.
final String baseIface = snapshot.getLinkProperties().getInterfaceName();
if (baseIface != null) {
+ nativeRegisterIface(baseIface);
findOrCreateNetworkIdentitySet(mActiveIfaces, baseIface).add(ident);
findOrCreateNetworkIdentitySet(mActiveUidIfaces, baseIface).add(ident);
@@ -2230,7 +2240,7 @@
.setDefaultNetwork(true)
.setOemManaged(ident.getOemManaged())
.setSubId(ident.getSubId()).build();
- final String ifaceVt = IFACE_VT + getSubIdForMobile(snapshot);
+ final String ifaceVt = IFACE_VT + getSubIdForCellularOrSatellite(snapshot);
findOrCreateNetworkIdentitySet(mActiveIfaces, ifaceVt).add(vtIdent);
findOrCreateNetworkIdentitySet(mActiveUidIfaces, ifaceVt).add(vtIdent);
}
@@ -2280,6 +2290,7 @@
// baseIface has been handled, so ignore it.
if (TextUtils.equals(baseIface, iface)) continue;
if (iface != null) {
+ nativeRegisterIface(iface);
findOrCreateNetworkIdentitySet(mActiveIfaces, iface).add(ident);
findOrCreateNetworkIdentitySet(mActiveUidIfaces, iface).add(ident);
if (isMobile) {
@@ -2298,9 +2309,15 @@
mMobileIfaces = mobileIfaces.toArray(new String[0]);
}
- private static int getSubIdForMobile(@NonNull NetworkStateSnapshot state) {
- if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
- throw new IllegalArgumentException("Mobile state need capability TRANSPORT_CELLULAR");
+ private static int getSubIdForCellularOrSatellite(@NonNull NetworkStateSnapshot state) {
+ if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
+ // Both cellular and satellite are 2 different network transport at Mobile using
+ // same telephony network specifier. So adding satellite transport to consider
+ // for, when satellite network is active at mobile.
+ && !state.getNetworkCapabilities().hasTransport(
+ NetworkCapabilities.TRANSPORT_SATELLITE)) {
+ throw new IllegalArgumentException(
+ "Mobile state need capability TRANSPORT_CELLULAR or TRANSPORT_SATELLITE");
}
final NetworkSpecifier spec = state.getNetworkCapabilities().getNetworkSpecifier();
@@ -2313,12 +2330,14 @@
}
/**
- * For networks with {@code TRANSPORT_CELLULAR}, get ratType that was obtained through
- * {@link PhoneStateListener}. Otherwise, return 0 given that other networks with different
- * transport types do not actually fill this value.
+ * For networks with {@code TRANSPORT_CELLULAR} Or {@code TRANSPORT_SATELLITE}, get ratType
+ * that was obtained through {@link PhoneStateListener}. Otherwise, return 0 given that other
+ * networks with different transport types do not actually fill this value.
*/
private int getRatTypeForStateSnapshot(@NonNull NetworkStateSnapshot state) {
- if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+ if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
+ && !state.getNetworkCapabilities()
+ .hasTransport(NetworkCapabilities.TRANSPORT_SATELLITE)) {
return 0;
}
@@ -2665,7 +2684,7 @@
@Override
protected void dump(FileDescriptor fd, PrintWriter rawWriter, String[] args) {
- if (!PermissionUtils.checkDumpPermission(mContext, TAG, rawWriter)) return;
+ if (!PermissionUtils.hasDumpPermission(mContext, TAG, rawWriter)) return;
long duration = DateUtils.DAY_IN_MILLIS;
final HashSet<String> argSet = new HashSet<String>();
@@ -2885,9 +2904,9 @@
}
pw.println();
- pw.println("InterfaceMapUpdater:");
+ pw.println("InterfaceMapHelper:");
pw.increaseIndent();
- mInterfaceMapUpdater.dump(pw);
+ mInterfaceMapHelper.dump(pw);
pw.decreaseIndent();
pw.println();
@@ -2909,6 +2928,12 @@
dumpStatsMapLocked(mStatsMapB, pw, "mStatsMapB");
dumpIfaceStatsMapLocked(pw);
pw.decreaseIndent();
+
+ pw.println();
+ pw.println("SkDestroyListener logs:");
+ pw.increaseIndent();
+ mSkDestroyListener.dump(pw);
+ pw.decreaseIndent();
}
}
@@ -3028,7 +3053,7 @@
BpfDump.dumpMap(statsMap, pw, mapName,
"ifaceIndex ifaceName tag_hex uid_int cnt_set rxBytes rxPackets txBytes txPackets",
(key, value) -> {
- final String ifName = mInterfaceMapUpdater.getIfNameByIndex(key.ifaceIndex);
+ final String ifName = mInterfaceMapHelper.getIfNameByIndex(key.ifaceIndex);
return key.ifaceIndex + " "
+ (ifName != null ? ifName : "unknown") + " "
+ "0x" + Long.toHexString(key.tag) + " "
@@ -3046,7 +3071,7 @@
BpfDump.dumpMap(mIfaceStatsMap, pw, "mIfaceStatsMap",
"ifaceIndex ifaceName rxBytes rxPackets txBytes txPackets",
(key, value) -> {
- final String ifName = mInterfaceMapUpdater.getIfNameByIndex(key.val);
+ final String ifName = mInterfaceMapHelper.getIfNameByIndex(key.val);
return key.val + " "
+ (ifName != null ? ifName : "unknown") + " "
+ value.rxBytes + " "
@@ -3407,6 +3432,7 @@
}
// TODO: Read stats by using BpfNetMapsReader.
+ private static native void nativeRegisterIface(String iface);
@Nullable
private static native NetworkStats.Entry nativeGetTotalStat();
@Nullable
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-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
new file mode 100644
index 0000000..ca97d07
--- /dev/null
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkStats;
+import android.util.LruCache;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.time.Clock;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
+ * with an adjustable expiry duration to manage data freshness.
+ */
+class TrafficStatsRateLimitCache {
+ private final Clock mClock;
+ private final long mExpiryDurationMs;
+
+ /**
+ * Constructs a new {@link TrafficStatsRateLimitCache} with the specified expiry duration.
+ *
+ * @param clock The {@link Clock} to use for determining timestamps.
+ * @param expiryDurationMs The expiry duration in milliseconds.
+ * @param maxSize Maximum number of entries.
+ */
+ TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs, int maxSize) {
+ mClock = clock;
+ mExpiryDurationMs = expiryDurationMs;
+ mMap = new LruCache<>(maxSize);
+ }
+
+ private static class TrafficStatsCacheKey {
+ @Nullable
+ public final String iface;
+ public final int uid;
+
+ TrafficStatsCacheKey(@Nullable String iface, int uid) {
+ this.iface = iface;
+ this.uid = uid;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof TrafficStatsCacheKey)) return false;
+ TrafficStatsCacheKey that = (TrafficStatsCacheKey) o;
+ return uid == that.uid && Objects.equals(iface, that.iface);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(iface, uid);
+ }
+ }
+
+ private static class TrafficStatsCacheValue {
+ public final long timestamp;
+ @NonNull
+ public final NetworkStats.Entry entry;
+
+ TrafficStatsCacheValue(long timestamp, NetworkStats.Entry entry) {
+ this.timestamp = timestamp;
+ this.entry = entry;
+ }
+ }
+
+ @GuardedBy("mMap")
+ private final LruCache<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap;
+
+ /**
+ * Retrieves a {@link NetworkStats.Entry} from the cache, associated with the given key.
+ *
+ * @param iface The interface name to include in the cache key. Null if not applicable.
+ * @param uid The UID to include in the cache key. {@code UID_ALL} if not applicable.
+ * @return The cached {@link NetworkStats.Entry}, or null if not found or expired.
+ */
+ @Nullable
+ NetworkStats.Entry get(String iface, int uid) {
+ final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
+ synchronized (mMap) { // Synchronize for thread-safety
+ final TrafficStatsCacheValue value = mMap.get(key);
+ if (value != null && !isExpired(value.timestamp)) {
+ return value.entry;
+ } else {
+ mMap.remove(key); // Remove expired entries
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Retrieves a {@link NetworkStats.Entry} from the cache, associated with the given key.
+ * If the entry is not found in the cache or has expired, computes it using the provided
+ * {@code supplier} and stores the result in the cache.
+ *
+ * @param iface The interface name to include in the cache key. {@code IFACE_ALL}
+ * if not applicable.
+ * @param uid The UID to include in the cache key. {@code UID_ALL} if not applicable.
+ * @param supplier The {@link Supplier} to compute the {@link NetworkStats.Entry} if not found.
+ * @return The cached or computed {@link NetworkStats.Entry}, or null if not found, expired,
+ * or if the {@code supplier} returns null.
+ */
+ @Nullable
+ NetworkStats.Entry getOrCompute(String iface, int uid,
+ @NonNull Supplier<NetworkStats.Entry> supplier) {
+ synchronized (mMap) {
+ final NetworkStats.Entry cachedValue = get(iface, uid);
+ if (cachedValue != null) {
+ return cachedValue;
+ }
+
+ // Entry not found or expired, compute it
+ final NetworkStats.Entry computedEntry = supplier.get();
+ if (computedEntry != null && !computedEntry.isEmpty()) {
+ put(iface, uid, computedEntry);
+ }
+ return computedEntry;
+ }
+ }
+
+ /**
+ * Stores a {@link NetworkStats.Entry} in the cache, associated with the given key.
+ *
+ * @param iface The interface name to include in the cache key. Null if not applicable.
+ * @param uid The UID to include in the cache key. {@code UID_ALL} if not applicable.
+ * @param entry The {@link NetworkStats.Entry} to store in the cache.
+ */
+ void put(String iface, int uid, @NonNull final NetworkStats.Entry entry) {
+ Objects.requireNonNull(entry);
+ final TrafficStatsCacheKey key = new TrafficStatsCacheKey(iface, uid);
+ synchronized (mMap) { // Synchronize for thread-safety
+ mMap.put(key, new TrafficStatsCacheValue(mClock.millis(), entry));
+ }
+ }
+
+ /**
+ * Clear the cache.
+ */
+ void clear() {
+ synchronized (mMap) {
+ mMap.evictAll();
+ }
+ }
+
+ private boolean isExpired(long timestamp) {
+ return mClock.millis() > timestamp + mExpiryDurationMs;
+ }
+}
diff --git a/service/Android.bp b/service/Android.bp
index a81386c..7c22ca5 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -70,9 +71,6 @@
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
@@ -181,6 +179,8 @@
"unsupportedappusage",
"ServiceConnectivityResources",
"framework-statsd",
+ "framework-permission",
+ "framework-permission-s",
],
static_libs: [
// Do not add libs here if they are already included
@@ -188,7 +188,7 @@
"androidx.annotation_annotation",
"connectivity-net-module-utils-bpf",
"connectivity_native_aidl_interface-lateststable-java",
- "dnsresolver_aidl_interface-V13-java",
+ "dnsresolver_aidl_interface-V14-java",
"modules-utils-shell-command-handler",
"net-utils-device-common",
"net-utils-device-common-ip",
@@ -199,13 +199,16 @@
"PlatformProperties",
"service-connectivity-protos",
"service-connectivity-stats-protos",
+ // The required dependency net-utils-device-common-struct-base is in the classpath via
+ // framework-connectivity
+ "net-utils-device-common-struct",
],
apex_available: [
"com.android.tethering",
],
lint: {
- strict_updatability_linting: true,
baseline_filename: "lint-baseline.xml",
+
},
visibility: [
"//packages/modules/Connectivity/service-t",
@@ -231,7 +234,7 @@
],
lint: {
strict_updatability_linting: true,
- baseline_filename: "lint-baseline.xml",
+
},
}
@@ -267,6 +270,8 @@
"framework-tethering.impl",
"framework-wifi",
"libprotobuf-java-nano",
+ "framework-permission",
+ "framework-permission-s",
],
jarjar_rules: ":connectivity-jarjar-rules",
apex_available: [
@@ -275,9 +280,6 @@
optimize: {
proguard_flags_files: ["proguard.flags"],
},
- lint: {
- strict_updatability_linting: true,
- },
}
// A special library created strictly for use by the tests as they need the
@@ -290,18 +292,12 @@
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 {
@@ -316,9 +312,6 @@
],
static_libs: ["ConnectivityServiceprotos"],
apex_available: ["com.android.tethering"],
- lint: {
- baseline_filename: "lint-baseline.xml",
- },
}
genrule {
diff --git a/service/ServiceConnectivityResources/Android.bp b/service/ServiceConnectivityResources/Android.bp
index 2260596..2621256 100644
--- a/service/ServiceConnectivityResources/Android.bp
+++ b/service/ServiceConnectivityResources/Android.bp
@@ -16,6 +16,7 @@
// APK to hold all the wifi overlayable resources.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/service/ServiceConnectivityResources/OWNERS b/service/ServiceConnectivityResources/OWNERS
new file mode 100644
index 0000000..df41ff2
--- /dev/null
+++ b/service/ServiceConnectivityResources/OWNERS
@@ -0,0 +1,2 @@
+per-file res/values/config_thread.xml = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
+per-file res/values/overlayable.xml = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/service/ServiceConnectivityResources/res/values-de/strings.xml b/service/ServiceConnectivityResources/res/values-de/strings.xml
index 536ebda..f58efb0 100644
--- a/service/ServiceConnectivityResources/res/values-de/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-de/strings.xml
@@ -29,7 +29,7 @@
<string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Für Optionen tippen"</string>
<string name="mobile_no_internet" msgid="4087718456753201450">"Mobiles Netzwerk hat keinen Internetzugriff"</string>
<string name="other_networks_no_internet" msgid="5693932964749676542">"Netzwerk hat keinen Internetzugriff"</string>
- <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Auf den privaten DNS-Server kann nicht zugegriffen werden"</string>
+ <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Auf den Server des privaten DNS kann nicht zugegriffen werden"</string>
<string name="network_partial_connectivity" msgid="5549503845834993258">"Schlechte Verbindung mit <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
<string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Tippen, um die Verbindung trotzdem herzustellen"</string>
<string name="network_switch_metered" msgid="5016937523571166319">"Zu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> gewechselt"</string>
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index 6f9d46f..2d3647a 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -128,6 +128,13 @@
<string-array translatable="false" name="config_networkNotifySwitches">
</string-array>
+ <!-- An array of priorities of service types for services to be offloaded via
+ NsdManager#registerOffloadEngine.
+ Format is [priority int]:[service type], for example: "0:_testservice._tcp"
+ -->
+ <string-array translatable="false" name="config_nsdOffloadServicesPriority">
+ </string-array>
+
<!-- Whether to use an ongoing notification for signing in to captive portals, instead of a
notification that can be dismissed. -->
<bool name="config_ongoingSignInNotification">false</bool>
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index 14b5427..4783f2b 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -20,10 +20,37 @@
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Sets to {@code true} to enable Thread on the device by default. Note this is the default
+ value, the actual Thread enabled state can be changed by the {@link
+ ThreadNetworkController#setEnabled} API.
+ -->
+ <bool name="config_thread_default_enabled">true</bool>
+
<!-- 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>
+ <!-- Specifies the UTF-8 vendor name of this device. If this value is not an empty string, it
+ will be included in TXT value (key is 'vn') of the "_meshcop._udp" mDNS service which is
+ published by the Thread service. A non-empty string value must not exceed length of 24 UTF-8
+ bytes.
+ -->
+ <string translatable="false" name="config_thread_vendor_name">Android</string>
+
+ <!-- Specifies the 24 bits vendor OUI of this device. If this value is not an empty string, it
+ will be included in TXT (key is 'vo') value of the "_meshcop._udp" mDNS service which is
+ published by the Thread service. The OUI can be represented as a base-16 number of six
+ hexadecimal digits, or octets separated by hyphens or dots. For example, "ACDE48", "AC-DE-48"
+ and "AC:DE:48" are all valid representations of the same OUI value.
+ -->
+ <string translatable="false" name="config_thread_vendor_oui"></string>
+
+ <!-- Specifies the UTF-8 product model name of this device. If this value is not an empty
+ string, it will be included in TXT (key is 'mn') value of the "_meshcop._udp" mDNS service
+ which is published by the Thread service. A non-empty string value must not exceed length of 24
+ UTF-8 bytes.
+ -->
+ <string translatable="false" name="config_thread_model_name">Thread Border Router</string>
</resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 1c07599..158b0c8 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -31,6 +31,7 @@
<item type="integer" name="config_networkWakeupPacketMask"/>
<item type="integer" name="config_networkNotifySwitchType"/>
<item type="array" name="config_networkNotifySwitches"/>
+ <item type="array" name="config_nsdOffloadServicesPriority"/>
<item type="bool" name="config_ongoingSignInNotification"/>
<item type="bool" name="config_autoCancelNetworkNotifications"/>
<item type="bool" name="config_notifyNoInternetAsDialogWhenHighPriority"/>
@@ -45,7 +46,11 @@
<item type="integer" name="config_netstats_validate_import" />
<!-- Configuration values for ThreadNetworkService -->
+ <item type="bool" name="config_thread_default_enabled" />
<item type="bool" name="config_thread_location_use_for_country_code_enabled" />
+ <item type="string" name="config_thread_vendor_name" />
+ <item type="string" name="config_thread_vendor_oui" />
+ <item type="string" name="config_thread_model_name" />
</policy>
</overlayable>
</resources>
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index c125bd6..4214bc9 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -113,7 +113,12 @@
if (!modules::sdklevel::IsAtLeastT()) return;
V("/sys/fs/bpf", S_IFDIR|S_ISVTX|0777, ROOT, ROOT, "fs_bpf", DIR);
- V("/sys/fs/bpf/net_shared", S_IFDIR|S_ISVTX|0777, ROOT, ROOT, "fs_bpf_net_shared", DIR);
+
+ if (false && modules::sdklevel::IsAtLeastV()) {
+ V("/sys/fs/bpf/net_shared", S_IFDIR|01777, ROOT, ROOT, "fs_bpf_net_shared", DIR);
+ } else {
+ V("/sys/fs/bpf/net_shared", S_IFDIR|01777, SYSTEM, SYSTEM, "fs_bpf_net_shared", DIR);
+ }
// pre-U we do not have selinux privs to getattr on bpf maps/progs
// so while the below *should* be as listed, we have no way to actually verify
diff --git a/service/libconnectivity/Android.bp b/service/libconnectivity/Android.bp
index e063af7..3a72134 100644
--- a/service/libconnectivity/Android.bp
+++ b/service/libconnectivity/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/service/lint-baseline.xml b/service/lint-baseline.xml
index 5149e6d..3e11d52 100644
--- a/service/lint-baseline.xml
+++ b/service/lint-baseline.xml
@@ -1,5 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `getUidRule`"
+ errorLine1=" return BpfNetMapsReader.getUidRule(sUidOwnerMap, childChain, uid);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/BpfNetMaps.java"
+ line="643"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `BpfBitmap`"
+ errorLine1=" return new BpfBitmap(BLOCKED_PORTS_MAP_PATH);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+ line="61"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `set`"
+ errorLine1=" mBpfBlockedPortsMap.set(port);"
+ errorLine2=" ~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+ line="96"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `unset`"
+ errorLine1=" mBpfBlockedPortsMap.unset(port);"
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+ line="107"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `clear`"
+ errorLine1=" mBpfBlockedPortsMap.clear();"
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+ line="118"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `get`"
+ errorLine1=" if (mBpfBlockedPortsMap.get(i)) portMap.add(i);"
+ errorLine2=" ~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ConnectivityNativeService.java"
+ line="131"
+ column="41"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportNetworkInterfaceForTransports`"
+ errorLine1=" batteryStats.reportNetworkInterfaceForTransports(iface, transportTypes);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1447"
+ column="26"/>
+ </issue>
<issue
id="NewApi"
@@ -8,23 +85,562 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="1358"
+ line="1458"
column="22"/>
</issue>
<issue
id="NewApi"
+ message="Call requires API level 33 (current min is 30): `getProgramId`"
+ errorLine1=" return BpfUtils.getProgramId(attachType);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1572"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
+ errorLine1=" mPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1740"
+ column="52"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#registerNetworkPolicyCallback`"
+ errorLine1=" mPolicyManager.registerNetworkPolicyCallback(null, mPolicyCallback);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1753"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Cast to `UidFrozenStateChangedCallback` requires API level 34 (current min is 30)"
+ errorLine1=" new UidFrozenStateChangedCallback() {"
+ errorLine2=" ^">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1888"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 34 (current min is 30): `android.app.ActivityManager.UidFrozenStateChangedCallback`"
+ errorLine1=" new UidFrozenStateChangedCallback() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1888"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 34 (current min is 30): `android.app.ActivityManager#registerUidFrozenStateChangedCallback`"
+ errorLine1=" activityManager.registerUidFrozenStateChangedCallback("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1907"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidNetworkingBlocked`"
+ errorLine1=" return mPolicyManager.isUidNetworkingBlocked(uid, metered);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2162"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getRestrictBackgroundStatus`"
+ errorLine1=" return mPolicyManager.getRestrictBackgroundStatus(callerUid);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2947"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
+ errorLine1=" final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(snapshot.getNetwork());"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2963"
+ column="81"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getLinkProperties`"
+ errorLine1=" snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2966"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetworkCapabilities`"
+ errorLine1=" snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2966"
+ column="64"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
+ errorLine1=" snapshot.getNetwork(), snapshot.getSubscriberId()));"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2967"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getSubscriberId`"
+ errorLine1=" snapshot.getNetwork(), snapshot.getSubscriberId()));"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2967"
+ column="57"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager.NetworkPolicyCallback`"
+ errorLine1=" private final NetworkPolicyCallback mPolicyCallback = new NetworkPolicyCallback() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="3210"
+ column="63"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `dump`"
+ errorLine1=" mBpfNetMaps.dump(pw, fd, verbose);"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="4155"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+ errorLine1=" if (!Build.isDebuggable()) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="5721"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
+ errorLine1=" mContext.getSystemService(NetworkPolicyManager.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="6174"
+ column="44"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getMultipathPreference`"
+ errorLine1=" networkPreference = netPolicyManager.getMultipathPreference(network);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="6179"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.UnderlyingNetworkInfo`"
+ errorLine1=" return new UnderlyingNetworkInfo(nai.networkCapabilities.getOwnerUid(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="6819"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidRestrictedOnMeteredNetworks`"
+ errorLine1=" if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="7822"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+ errorLine1=" if (Build.isDebuggable()) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="9943"
+ column="23"/>
+ </issue>
+
+ <issue
+ id="NewApi"
message="Call requires API level 31 (current min is 30): `android.app.usage.NetworkStatsManager#notifyNetworkStatus`"
errorLine1=" mStatsManager.notifyNetworkStatus(getDefaultNetworks(),"
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="9938"
+ line="10909"
column="27"/>
</issue>
<issue
id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(pfd);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="10962"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(pfd);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="10979"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager`"
+ errorLine1=" NetworkWatchlistManager nwm = mContext.getSystemService(NetworkWatchlistManager.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="11035"
+ column="65"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager#getWatchlistConfigHash`"
+ errorLine1=" return nwm.getWatchlistConfigHash();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="11041"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `getProgramId`"
+ errorLine1=" final int ret = BpfUtils.getProgramId(type);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="11180"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportMobileRadioPowerState`"
+ errorLine1=" bs.reportMobileRadioPowerState(isActive, uid);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="12254"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportWifiRadioPowerState`"
+ errorLine1=" bs.reportWifiRadioPowerState(isActive, uid);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="12257"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `addNiceApp`"
+ errorLine1=" mBpfNetMaps.addNiceApp(uid);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13079"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `removeNiceApp`"
+ errorLine1=" mBpfNetMaps.removeNiceApp(uid);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13081"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `addNaughtyApp`"
+ errorLine1=" mBpfNetMaps.addNaughtyApp(uid);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13094"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `removeNaughtyApp`"
+ errorLine1=" mBpfNetMaps.removeNaughtyApp(uid);"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13096"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+ errorLine1=" final int uid = uh.getUid(appId);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13112"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `setUidRule`"
+ errorLine1=" mBpfNetMaps.setUidRule(chain, uid, firewallRule);"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13130"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `setChildChain`"
+ errorLine1=" mBpfNetMaps.setChildChain(chain, enable);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13195"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `isChainEnabled`"
+ errorLine1=" return mBpfNetMaps.isChainEnabled(chain);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13213"
+ column="28"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 33 (current min is 30): `replaceUidChain`"
+ errorLine1=" mBpfNetMaps.replaceUidChain(chain, uids);"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="13220"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `BpfMap`"
+ errorLine1=" mBpfDscpIpv4Policies = new BpfMap<Struct.S32, DscpPolicyValue>(IPV4_POLICY_MAP_PATH,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+ line="88"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `BpfMap`"
+ errorLine1=" mBpfDscpIpv6Policies = new BpfMap<Struct.S32, DscpPolicyValue>(IPV6_POLICY_MAP_PATH,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+ line="90"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `insertOrReplaceEntry`"
+ errorLine1=" mBpfDscpIpv4Policies.insertOrReplaceEntry("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+ line="183"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `insertOrReplaceEntry`"
+ errorLine1=" mBpfDscpIpv6Policies.insertOrReplaceEntry("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+ line="194"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `replaceEntry`"
+ errorLine1=" mBpfDscpIpv4Policies.replaceEntry(new Struct.S32(index), DscpPolicyValue.NONE);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+ line="261"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `replaceEntry`"
+ errorLine1=" mBpfDscpIpv6Policies.replaceEntry(new Struct.S32(index), DscpPolicyValue.NONE);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyTracker.java"
+ line="262"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+ errorLine1=' InetAddress.parseNumericAddress("::").getAddress();'
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyValue.java"
+ line="99"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.NetworkStateSnapshot`"
+ errorLine1=" return new NetworkStateSnapshot(network, new NetworkCapabilities(networkCapabilities),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkAgentInfo.java"
+ line="1353"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(mFileDescriptor);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkDiagnostics.java"
+ line="570"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Field requires API level 31 (current min is 30): `android.os.Build.VERSION#DEVICE_INITIAL_SDK_INT`"
+ errorLine1=" return Build.VERSION.DEVICE_INITIAL_SDK_INT;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="212"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
+ errorLine1=" for (final int uid : mSystemConfigManager.getSystemPermissionUids(INTERNET)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="396"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
+ errorLine1=" for (final int uid : mSystemConfigManager.getSystemPermissionUids(UPDATE_DEVICE_STATS)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="404"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="NewApi"
message="Call requires API level 31 (current min is 30): `android.content.pm.ApplicationInfo#isOem`"
errorLine1=" return appInfo.isVendor() || appInfo.isOem() || appInfo.isProduct();"
errorLine2=" ~~~~~">
@@ -58,441 +674,34 @@
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getMultipathPreference`"
- errorLine1=" networkPreference = netPolicyManager.getMultipathPreference(network);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="5498"
- column="50"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getRestrictBackgroundStatus`"
- errorLine1=" return mPolicyManager.getRestrictBackgroundStatus(callerUid);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="2565"
- column="35"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidNetworkingBlocked`"
- errorLine1=" return mPolicyManager.isUidNetworkingBlocked(uid, metered);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="1914"
- column="35"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidRestrictedOnMeteredNetworks`"
- errorLine1=" if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="7094"
- column="32"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#registerNetworkPolicyCallback`"
- errorLine1=" mPolicyManager.registerNetworkPolicyCallback(null, mPolicyCallback);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="1567"
- column="24"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getLinkProperties`"
- errorLine1=" snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="2584"
- column="34"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetworkCapabilities`"
- errorLine1=" snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="2584"
- column="64"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
- errorLine1=" snapshot.getNetwork(), snapshot.getSubscriberId()));"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="2585"
- column="34"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
- errorLine1=" final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(snapshot.getNetwork());"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="2581"
- column="81"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getSubscriberId`"
- errorLine1=" snapshot.getNetwork(), snapshot.getSubscriberId()));"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="2585"
- column="57"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager#getWatchlistConfigHash`"
- errorLine1=" return nwm.getWatchlistConfigHash();"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="10060"
- column="20"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#addPacProxyInstalledListener`"
- errorLine1=" mPacProxyManager.addPacProxyInstalledListener("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
- line="111"
- column="26"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
- errorLine1=" () -> mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties));"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
- line="208"
- column="48"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
- errorLine1=" mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
- line="252"
- column="26"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportMobileRadioPowerState`"
- errorLine1=" bs.reportMobileRadioPowerState(isActive, NO_UID);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="11006"
- column="24"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportNetworkInterfaceForTransports`"
- errorLine1=" batteryStats.reportNetworkInterfaceForTransports(iface, transportTypes);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="1347"
- column="26"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportWifiRadioPowerState`"
- errorLine1=" bs.reportWifiRadioPowerState(isActive, NO_UID);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="11009"
- column="24"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
- errorLine1=" if (Build.isDebuggable()) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="9074"
- column="23"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
- errorLine1=" if (!Build.isDebuggable()) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="5039"
- column="20"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
- errorLine1=" for (final int uid : mSystemConfigManager.getSystemPermissionUids(INTERNET)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
- line="396"
- column="51"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
- errorLine1=" for (final int uid : mSystemConfigManager.getSystemPermissionUids(UPDATE_DEVICE_STATS)) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
- line="404"
- column="51"/>
- </issue>
-
- <issue
- id="NewApi"
message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
errorLine1=" final int uid = handle.getUid(appId);"
errorLine2=" ~~~~~~">
<location
file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
- line="1069"
+ line="1070"
column="44"/>
</issue>
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
- errorLine1=" tcpDetails.tos = Os.getsockoptInt(fd, IPPROTO_IP, IP_TOS);"
- errorLine2=" ~~~~~~~~~~~~~">
+ message="Call requires API level 33 (current min is 30): `updateUidLockdownRule`"
+ errorLine1=" mBpfNetMaps.updateUidLockdownRule(uid, add);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
<location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
- line="285"
- column="37"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
- errorLine1=" tcpDetails.ttl = Os.getsockoptInt(fd, IPPROTO_IP, IP_TTL);"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
- line="287"
- column="37"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
- errorLine1=" tcpDetails.ack = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
- line="265"
- column="33"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
- errorLine1=" tcpDetails.seq = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
- line="262"
- column="33"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
- errorLine1=" final int result = Os.ioctlInt(fd, SIOCINQ);"
- errorLine2=" ~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
- line="392"
- column="31"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
- errorLine1=" final int result = Os.ioctlInt(fd, SIOCOUTQ);"
- errorLine2=" ~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
- line="402"
- column="31"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
- errorLine1=' InetAddress.parseNumericAddress("::").getAddress();'
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyValue.java"
- line="99"
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="1123"
column="25"/>
</issue>
<issue
id="NewApi"
- message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
- errorLine1=' private static final InetAddress GOOGLE_DNS_4 = InetAddress.parseNumericAddress("8.8.8.8");'
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ClatCoordinator.java"
- line="89"
- column="65"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(pfd);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="9991"
- column="25"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(pfd);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="10008"
- column="25"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
- errorLine1=" IoUtils.closeQuietly(mFileDescriptor);"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkDiagnostics.java"
- line="481"
- column="21"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `new android.net.NetworkStateSnapshot`"
- errorLine1=" return new NetworkStateSnapshot(network, new NetworkCapabilities(networkCapabilities),"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkAgentInfo.java"
- line="1269"
- column="20"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 30): `new android.net.UnderlyingNetworkInfo`"
- errorLine1=" return new UnderlyingNetworkInfo(nai.networkCapabilities.getOwnerUid(),"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="6123"
- column="16"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager.NetworkPolicyCallback`"
- errorLine1=" private final NetworkPolicyCallback mPolicyCallback = new NetworkPolicyCallback() {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="2827"
- column="63"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
- errorLine1=" mContext.getSystemService(NetworkPolicyManager.class);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="5493"
- column="44"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
- errorLine1=" mPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="1554"
- column="52"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Class requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager`"
- errorLine1=" NetworkWatchlistManager nwm = mContext.getSystemService(NetworkWatchlistManager.class);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
- line="10054"
- column="65"/>
- </issue>
-
- <issue
- id="NewApi"
message="Class requires API level 31 (current min is 30): `android.net.PacProxyManager.PacProxyInstalledListener`"
errorLine1=" private class PacProxyInstalledListener implements PacProxyManager.PacProxyInstalledListener {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
- line="90"
+ line="92"
column="56"/>
</issue>
@@ -503,8 +712,107 @@
errorLine2=" ~~~~~~~~~~~~~~~">
<location
file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
- line="108"
+ line="111"
column="53"/>
</issue>
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#addPacProxyInstalledListener`"
+ errorLine1=" mPacProxyManager.addPacProxyInstalledListener("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="115"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
+ errorLine1=" () -> mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="213"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
+ errorLine1=" mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="259"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.seq = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="269"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.ack = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="272"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.tos = Os.getsockoptInt(fd, IPPROTO_IP, IP_TOS);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="292"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.ttl = Os.getsockoptInt(fd, IPPROTO_IP, IP_TTL);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="294"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
+ errorLine1=" final int result = Os.ioctlInt(fd, SIOCINQ);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="401"
+ column="31"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
+ errorLine1=" final int result = Os.ioctlInt(fd, SIOCOUTQ);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="411"
+ column="31"/>
+ </issue>
+
</issues>
\ No newline at end of file
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
index 5c6b123..6c1c2c4 100644
--- a/service/native/libs/libclat/Android.bp
+++ b/service/native/libs/libclat/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -38,7 +39,10 @@
cc_test {
name: "libclat_test",
defaults: ["netd_defaults"],
- test_suites: ["general-tests", "mts-tethering"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
test_config_template: ":net_native_test_config_template",
srcs: [
"clatutils_test.cpp",
diff --git a/service/proguard.flags b/service/proguard.flags
index cf25f05..ed9a65f 100644
--- a/service/proguard.flags
+++ b/service/proguard.flags
@@ -15,3 +15,7 @@
static final % EVENT_*;
}
+# b/313539492 Keep the onLocalNetworkInfoChanged method in classes extending Connectivity.NetworkCallback.
+-keepclassmembers class * extends **android.net.ConnectivityManager$NetworkCallback {
+ public void onLocalNetworkInfoChanged(**android.net.Network, **android.net.LocalNetworkInfo);
+}
diff --git a/service/src/com/android/server/BpfLoaderRcUtils.java b/service/src/com/android/server/BpfLoaderRcUtils.java
deleted file mode 100644
index 293e757..0000000
--- a/service/src/com/android/server/BpfLoaderRcUtils.java
+++ /dev/null
@@ -1,161 +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;
-
-import android.annotation.NonNull;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.modules.utils.build.SdkLevel;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * BpfRcUtils is responsible for comparing the bpf loader rc file.
- *
- * {@hide}
- */
-public class BpfLoaderRcUtils {
- public static final String TAG = BpfLoaderRcUtils.class.getSimpleName();
-
- private static final List<String> BPF_LOADER_RC_S_T = List.of(
- "service bpfloader /system/bin/bpfloader",
- "capabilities CHOWN SYS_ADMIN NET_ADMIN",
- "rlimit memlock 1073741824 1073741824",
- "oneshot",
- "reboot_on_failure reboot,bpfloader-failed",
- "updatable"
- );
-
- private static final List<String> BPF_LOADER_RC_U = List.of(
- "service bpfloader /system/bin/bpfloader",
- "capabilities CHOWN SYS_ADMIN NET_ADMIN",
- "group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system",
- "user root",
- "rlimit memlock 1073741824 1073741824",
- "oneshot",
- "reboot_on_failure reboot,bpfloader-failed",
- "updatable"
- );
-
- private static final List<String> BPF_LOADER_RC_UQPR2 = List.of(
- "service bpfloader /system/bin/netbpfload",
- "capabilities CHOWN SYS_ADMIN NET_ADMIN",
- "group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system",
- "user root",
- "rlimit memlock 1073741824 1073741824",
- "oneshot",
- "reboot_on_failure reboot,bpfloader-failed",
- "updatable"
- );
-
-
- private static final String BPF_LOADER_RC_FILE_PATH = "/etc/init/bpfloader.rc";
- private static final String NET_BPF_LOAD_RC_FILE_PATH = "/etc/init/netbpfload.rc";
-
- private BpfLoaderRcUtils() {
- }
-
- /**
- * Load the bpf rc file content from the input stream.
- */
- @VisibleForTesting
- public static List<String> loadExistingBpfRcFile(@NonNull InputStream inputStream) {
- List<String> contents = new ArrayList<>();
- boolean bpfSectionFound = false;
- try (BufferedReader br = new BufferedReader(
- new InputStreamReader(inputStream, StandardCharsets.ISO_8859_1))) {
- String line;
- while ((line = br.readLine()) != null) {
- line = line.trim();
- if (line.isEmpty()) {
- continue;
- }
- if (line.startsWith("#")) {
- continue;
- }
- // If bpf service section was found and new service or action section start. The
- // read should stop.
- if (bpfSectionFound && (line.startsWith("service ") || (line.startsWith("on ")))) {
- break;
- }
- if (line.startsWith("service bpfloader ")) {
- bpfSectionFound = true;
- }
- if (bpfSectionFound) {
- contents.add(line);
- }
- }
- } catch (IOException e) {
- Log.wtf("read input stream failed.", e);
- contents.clear();
- return contents;
- }
- return contents;
- }
-
- /**
- * Check the bpfLoader rc file on the system image matches any of the template files.
- */
- public static boolean checkBpfLoaderRc() {
- File bpfRcFile = new File(BPF_LOADER_RC_FILE_PATH);
- if (!bpfRcFile.exists()) {
- if (SdkLevel.isAtLeastU()) {
- bpfRcFile = new File(NET_BPF_LOAD_RC_FILE_PATH);
- }
- if (!bpfRcFile.exists()) {
- Log.wtf(TAG,
- "neither " + BPF_LOADER_RC_FILE_PATH + " nor " + NET_BPF_LOAD_RC_FILE_PATH
- + " exist.");
- return false;
- }
- // Check bpf rc file in U QPR2
- return compareBpfLoaderRc(bpfRcFile, BPF_LOADER_RC_UQPR2);
- }
-
- if (SdkLevel.isAtLeastU()) {
- // Check bpf rc file in U
- return compareBpfLoaderRc(bpfRcFile, BPF_LOADER_RC_U);
- }
- // Check bpf rc file in S/T
- return compareBpfLoaderRc(bpfRcFile, BPF_LOADER_RC_S_T);
- }
-
- private static boolean compareBpfLoaderRc(@NonNull File bpfRcFile,
- @NonNull List<String> template) {
- try {
- List<String> actualContent = loadExistingBpfRcFile(new FileInputStream(bpfRcFile));
- if (!actualContent.equals(template)) {
- Log.wtf(TAG, "BPF rc file is not same as the template files " + actualContent);
- return false;
- }
- } catch (FileNotFoundException e) {
- Log.wtf(bpfRcFile.getPath() + " doesn't exist.", e);
- return false;
- }
- return true;
- }
-}
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index ad9cfbe..42c1628 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -31,7 +31,6 @@
import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
import static android.net.BpfNetMapsConstants.UID_PERMISSION_MAP_PATH;
import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
-import static android.net.BpfNetMapsUtils.PRE_T;
import static android.net.BpfNetMapsUtils.getMatchByFirewallChain;
import static android.net.BpfNetMapsUtils.isFirewallAllowList;
import static android.net.BpfNetMapsUtils.matchToString;
@@ -50,7 +49,7 @@
import android.app.StatsManager;
import android.content.Context;
-import android.net.BpfNetMapsReader;
+import android.net.BpfNetMapsUtils;
import android.net.INetd;
import android.net.UidOwnerValue;
import android.os.Build;
@@ -68,6 +67,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.BackgroundThread;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.BpfDump;
import com.android.net.module.util.BpfMap;
import com.android.net.module.util.IBpfMap;
@@ -95,7 +95,7 @@
*/
public class BpfNetMaps {
static {
- if (!PRE_T) {
+ if (SdkLevel.isAtLeastT()) {
System.loadLibrary("service-connectivity");
}
}
@@ -184,60 +184,67 @@
sIngressDiscardMap = ingressDiscardMap;
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
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);
}
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
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);
}
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
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);
}
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
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);
}
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
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);
}
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
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);
}
}
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static void initBpfMaps() {
if (sConfigurationMap == null) {
sConfigurationMap = getConfigurationMap();
@@ -295,6 +302,7 @@
* Initializes the class if it is not already initialized. This method will open maps but not
* cause any other effects. This method may be called multiple times on any thread.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static synchronized void ensureInitialized(final Context context) {
if (sInitialized) return;
initBpfMaps();
@@ -348,7 +356,7 @@
public BpfNetMaps(final Context context) {
this(context, null);
- if (PRE_T) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
+ if (!SdkLevel.isAtLeastT()) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
}
public BpfNetMaps(final Context context, final INetd netd) {
@@ -357,7 +365,7 @@
@VisibleForTesting
public BpfNetMaps(final Context context, final INetd netd, final Dependencies deps) {
- if (!PRE_T) {
+ if (SdkLevel.isAtLeastT()) {
ensureInitialized(context);
}
mNetd = netd;
@@ -371,7 +379,7 @@
}
private void throwIfPreT(final String msg) {
- if (PRE_T) {
+ if (!SdkLevel.isAtLeastT()) {
throw new UnsupportedOperationException(msg);
}
}
@@ -527,14 +535,11 @@
* @throws UnsupportedOperationException if called on pre-T devices.
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
- *
- * @deprecated Use {@link BpfNetMapsReader#isChainEnabled} instead.
*/
- // TODO: Migrate the callers to use {@link BpfNetMapsReader#isChainEnabled} instead.
@Deprecated
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public boolean isChainEnabled(final int childChain) {
- return BpfNetMapsReader.isChainEnabled(sConfigurationMap, childChain);
+ return BpfNetMapsUtils.isChainEnabled(sConfigurationMap, childChain);
}
private Set<Integer> asSet(final int[] uids) {
@@ -627,12 +632,9 @@
* @throws UnsupportedOperationException if called on pre-T devices.
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
- *
- * @deprecated use {@link BpfNetMapsReader#getUidRule} instead.
*/
- // TODO: Migrate the callers to use {@link BpfNetMapsReader#getUidRule} instead.
public int getUidRule(final int childChain, final int uid) {
- return BpfNetMapsReader.getUidRule(sUidOwnerMap, childChain, uid);
+ return BpfNetMapsUtils.getUidRule(sUidOwnerMap, childChain, uid);
}
private Set<Integer> getUidsMatchEnabled(final int childChain) throws ErrnoException {
@@ -712,7 +714,7 @@
* cause of the failure.
*/
public void addUidInterfaceRules(final String ifName, final int[] uids) throws RemoteException {
- if (PRE_T) {
+ if (!SdkLevel.isAtLeastT()) {
mNetd.firewallAddUidInterfaceRules(ifName, uids);
return;
}
@@ -750,7 +752,7 @@
* cause of the failure.
*/
public void removeUidInterfaceRules(final int[] uids) throws RemoteException {
- if (PRE_T) {
+ if (!SdkLevel.isAtLeastT()) {
mNetd.firewallRemoveUidInterfaceRules(uids);
return;
}
@@ -829,7 +831,7 @@
* @throws RemoteException when netd has crashed.
*/
public void setNetPermForUids(final int permissions, final int[] uids) throws RemoteException {
- if (PRE_T) {
+ if (!SdkLevel.isAtLeastT()) {
mNetd.trafficSetNetPermForUids(permissions, uids);
return;
}
@@ -916,6 +918,25 @@
}
}
+ /**
+ * Return whether the network is blocked by firewall chains for the given uid.
+ *
+ * Note that {@link #getDataSaverEnabled()} has a latency before V.
+ *
+ * @param uid The target uid.
+ * @param isNetworkMetered Whether the target network is metered.
+ *
+ * @return True if the network is blocked. Otherwise, false.
+ * @throws ServiceSpecificException if the read fails.
+ *
+ * @hide
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ public boolean isUidNetworkingBlocked(final int uid, boolean isNetworkMetered) {
+ return BpfNetMapsUtils.isUidNetworkingBlocked(uid, isNetworkMetered,
+ sConfigurationMap, sUidOwnerMap, sDataSaverEnabledMap);
+ }
+
/** Register callback for statsd to pull atom. */
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void setPullAtomCallback(final Context context) {
@@ -1019,7 +1040,7 @@
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void dump(final IndentingPrintWriter pw, final FileDescriptor fd, boolean verbose)
throws IOException, ServiceSpecificException {
- if (PRE_T) {
+ if (!SdkLevel.isAtLeastT()) {
throw new ServiceSpecificException(
EOPNOTSUPP, "dumpsys connectivity trafficcontroller dump not available on pre-T"
+ " devices, use dumpsys netd trafficcontroller instead.");
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 6b47654..b1ae019 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -38,6 +38,7 @@
import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
import static android.net.ConnectivityManager.CALLBACK_IP_CHANGED;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
import static android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT;
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
@@ -65,6 +66,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;
@@ -95,6 +97,8 @@
import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY;
import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST;
import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST_ONLY;
+import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_LOCAL_NETWORK;
+import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_SELF_CERTIFIED_CAPABILITIES_DECLARATION;
import static android.os.Process.INVALID_UID;
import static android.os.Process.VPN_UID;
import static android.system.OsConstants.ETH_P_ALL;
@@ -107,15 +111,18 @@
import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_INGRESS;
import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_SOCK_CREATE;
import static com.android.net.module.util.NetworkMonitorUtils.isPrivateDnsValidationRequired;
-import static com.android.net.module.util.PermissionUtils.checkAnyPermissionOf;
import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf;
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission;
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr;
+import static com.android.net.module.util.PermissionUtils.hasAnyPermissionOf;
import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
+import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
+import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
import static java.util.Map.Entry;
import android.Manifest;
+import android.annotation.CheckResult;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
@@ -171,6 +178,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;
@@ -211,7 +219,6 @@
import android.net.Uri;
import android.net.VpnManager;
import android.net.VpnTransportInfo;
-import android.net.connectivity.ConnectivityCompatChanges;
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.NetworkEvent;
import android.net.netd.aidl.NativeUidRangeConfig;
@@ -253,6 +260,7 @@
import android.stats.connectivity.ValidatedState;
import android.sysprop.NetworkProperties;
import android.system.ErrnoException;
+import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -281,7 +289,6 @@
import com.android.metrics.NetworkDescription;
import com.android.metrics.NetworkList;
import com.android.metrics.NetworkRequestCount;
-import com.android.metrics.NetworkRequestStateStatsMetrics;
import com.android.metrics.RequestCountForType;
import com.android.modules.utils.BasicShellCommandHandler;
import com.android.modules.utils.build.SdkLevel;
@@ -291,6 +298,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;
@@ -315,12 +323,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;
@@ -329,11 +337,13 @@
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;
import com.android.server.connectivity.QosCallbackTracker;
import com.android.server.connectivity.RoutingCoordinatorService;
+import com.android.server.connectivity.SatelliteAccessController;
import com.android.server.connectivity.UidRangeUtils;
import com.android.server.connectivity.VpnNetworkPreferenceInfo;
import com.android.server.connectivity.wear.CompanionDeviceManagerProxyService;
@@ -362,6 +372,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;
@@ -370,6 +381,8 @@
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
/**
* @hide
@@ -464,6 +477,9 @@
private volatile boolean mLockdownEnabled;
+ private final boolean mRequestRestrictedWifiEnabled;
+ private final boolean mBackgroundFirewallChainEnabled;
+
/**
* Stale copy of uid blocked reasons provided by NPMS. As long as they are accessed only in
* internal handler thread, they don't need a lock.
@@ -497,6 +513,7 @@
@GuardedBy("mTNSLock")
private TestNetworkService mTNS;
private final CompanionDeviceManagerProxyService mCdmps;
+ private final MulticastRoutingCoordinatorService mMulticastRoutingCoordinatorService;
private final RoutingCoordinatorService mRoutingCoordinatorService;
private final Object mTNSLock = new Object();
@@ -558,6 +575,10 @@
// See {@link ConnectivitySettingsManager#setMobileDataPreferredUids}
@VisibleForTesting
static final int PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED = 30;
+ // Order of setting satellite network preference fallback when default message application
+ // with role_sms role and android.permission.SATELLITE_COMMUNICATION permission detected
+ @VisibleForTesting
+ static final int PREFERENCE_ORDER_SATELLITE_FALLBACK = 40;
// Preference order that signifies the network shouldn't be set as a default network for
// the UIDs, only give them access to it. TODO : replace this with a boolean
// in NativeUidRangeConfig
@@ -916,6 +937,7 @@
private final QosCallbackTracker mQosCallbackTracker;
private final NetworkNotificationManager mNotifier;
private final LingerMonitor mLingerMonitor;
+ private final SatelliteAccessController mSatelliteAccessController;
// sequence number of NetworkRequests
private int mNextNetworkRequestId = NetworkRequest.FIRST_REQUEST_ID;
@@ -942,7 +964,7 @@
private final IpConnectivityLog mMetricsLog;
- private final NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
+ @Nullable private final NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
@GuardedBy("mBandwidthRequests")
private final SparseArray<Integer> mBandwidthRequests = new SparseArray<>(10);
@@ -979,6 +1001,9 @@
// Uids that ConnectivityService is pending to close sockets of.
private final Set<Integer> mPendingFrozenUids = new ArraySet<>();
+ // Flag to drop packets to VPN addresses ingressing via non-VPN interfaces.
+ private final boolean mIngressToVpnAddressFiltering;
+
/**
* Implements support for the legacy "one network per network type" model.
*
@@ -1280,7 +1305,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)) {
@@ -1289,7 +1314,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");
}
@@ -1424,6 +1449,17 @@
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
*/
@@ -1431,7 +1467,7 @@
Context context) {
// We currently have network requests metric for Watch devices only
if (context.getPackageManager().hasSystemFeature(FEATURE_WATCH)) {
- return new NetworkRequestStateStatsMetrics();
+ return new NetworkRequestStateStatsMetrics();
} else {
return null;
}
@@ -1473,15 +1509,32 @@
*/
@Nullable
public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
- @NonNull final Context context, @NonNull final TelephonyManager tm) {
+ @NonNull final Context context,
+ @NonNull final TelephonyManager tm,
+ boolean requestRestrictedWifiEnabled,
+ @NonNull BiConsumer<Integer, Integer> listener,
+ @NonNull final Handler connectivityServiceHandler) {
if (isAtLeastT()) {
- return new CarrierPrivilegeAuthenticator(context, tm);
+ return new CarrierPrivilegeAuthenticator(context, tm, requestRestrictedWifiEnabled,
+ listener, connectivityServiceHandler);
} else {
return null;
}
}
/**
+ * @see SatelliteAccessController
+ */
+ @Nullable
+ public SatelliteAccessController makeSatelliteAccessController(
+ @NonNull final Context context,
+ Consumer<Set<Integer>> updateSatelliteNetworkFallbackUidCallback,
+ @NonNull final Handler connectivityServiceInternalHandler) {
+ return new SatelliteAccessController(context, updateSatelliteNetworkFallbackUidCallback,
+ connectivityServiceInternalHandler);
+ }
+
+ /**
* @see DeviceConfigUtils#isTetheringFeatureEnabled
*/
public boolean isFeatureEnabled(Context context, String name) {
@@ -1744,8 +1797,22 @@
mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
mLocationPermissionChecker = mDeps.makeLocationPermissionChecker(mContext);
- mCarrierPrivilegeAuthenticator =
- mDeps.makeCarrierPrivilegeAuthenticator(mContext, mTelephonyManager);
+ mRequestRestrictedWifiEnabled = mDeps.isAtLeastU()
+ && mDeps.isFeatureEnabled(context, REQUEST_RESTRICTED_WIFI);
+ mBackgroundFirewallChainEnabled = mDeps.isAtLeastV() && mDeps.isFeatureNotChickenedOut(
+ context, ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN);
+ mCarrierPrivilegeAuthenticator = mDeps.makeCarrierPrivilegeAuthenticator(
+ mContext, mTelephonyManager, mRequestRestrictedWifiEnabled,
+ this::handleUidCarrierPrivilegesLost, mHandler);
+
+ if (mDeps.isAtLeastU()
+ && mDeps
+ .isFeatureNotChickenedOut(mContext, ALLOW_SATALLITE_NETWORK_FALLBACK)) {
+ mSatelliteAccessController = mDeps.makeSatelliteAccessController(
+ mContext, this::updateSatelliteNetworkPreferenceUids, mHandler);
+ } else {
+ mSatelliteAccessController = null;
+ }
// To ensure uid state is synchronized with Network Policy, register for
// NetworkPolicyManagerService events must happen prior to NetworkPolicyManagerService
@@ -1876,9 +1943,11 @@
}
mRoutingCoordinatorService = new RoutingCoordinatorService(netd);
+ mMulticastRoutingCoordinatorService =
+ mDeps.makeMulticastRoutingCoordinatorService(mHandler);
- mDestroyFrozenSockets = mDeps.isAtLeastU()
- && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION);
+ mDestroyFrozenSockets = mDeps.isAtLeastV() || (mDeps.isAtLeastU()
+ && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION));
mDelayDestroyFrozenSockets = mDeps.isAtLeastU()
&& mDeps.isFeatureEnabled(context, DELAY_DESTROY_FROZEN_SOCKETS_VERSION);
mAllowSysUiConnectivityReports = mDeps.isFeatureNotChickenedOut(
@@ -1907,10 +1976,8 @@
activityManager.registerUidFrozenStateChangedCallback(
(Runnable r) -> r.run(), frozenStateChangedCallback);
}
-
- if (mDeps.isFeatureNotChickenedOut(mContext, LOG_BPF_RC)) {
- mHandler.post(BpfLoaderRcUtils::checkBpfLoaderRc);
- }
+ mIngressToVpnAddressFiltering = mDeps.isAtLeastT()
+ && mDeps.isFeatureNotChickenedOut(mContext, INGRESS_TO_VPN_ADDRESS_FILTERING);
}
/**
@@ -2013,6 +2080,18 @@
new Pair<>(network, proxyInfo)).sendToTarget();
}
+ /**
+ * Called when satellite network fallback uids at {@link SatelliteAccessController}
+ * cache was updated based on {@link
+ * android.app.role.OnRoleHoldersChangedListener#onRoleHoldersChanged(String, UserHandle)},
+ * to create multilayer request with preference order
+ * {@link #PREFERENCE_ORDER_SATELLITE_FALLBACK} there on.
+ *
+ */
+ private void updateSatelliteNetworkPreferenceUids(Set<Integer> satelliteNetworkFallbackUids) {
+ handleSetSatelliteNetworkPreference(satelliteNetworkFallbackUids);
+ }
+
private void handleAlwaysOnNetworkRequest(
NetworkRequest networkRequest, String settingName, boolean defaultValue) {
final boolean enable = toBool(Settings.Global.getInt(
@@ -2159,7 +2238,11 @@
final long ident = Binder.clearCallingIdentity();
try {
final boolean metered = nc == null ? true : nc.isMetered();
- return mPolicyManager.isUidNetworkingBlocked(uid, metered);
+ if (mDeps.isAtLeastV()) {
+ return mBpfNetMaps.isUidNetworkingBlocked(uid, metered);
+ } else {
+ return mPolicyManager.isUidNetworkingBlocked(uid, metered);
+ }
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -2528,7 +2611,7 @@
// Not the system, so it's an app requesting on its own behalf.
type = RequestType.RT_APP.getNumber();
}
- countPerType.put(type, countPerType.get(type, 0));
+ countPerType.put(type, countPerType.get(type, 0) + 1);
}
for (int i = countPerType.size() - 1; i >= 0; --i) {
final RequestCountForType.Builder r = RequestCountForType.newBuilder();
@@ -2632,7 +2715,7 @@
Objects.requireNonNull(packageName);
Objects.requireNonNull(lp);
enforceNetworkStackOrSettingsPermission();
- if (!checkAccessPermission(-1 /* pid */, uid)) {
+ if (!hasAccessPermission(-1 /* pid */, uid)) {
return null;
}
return linkPropertiesRestrictedForCallerPermissions(lp, -1 /* callerPid */, uid);
@@ -2668,7 +2751,7 @@
Objects.requireNonNull(nc);
Objects.requireNonNull(packageName);
enforceNetworkStackOrSettingsPermission();
- if (!checkAccessPermission(-1 /* pid */, uid)) {
+ if (!hasAccessPermission(-1 /* pid */, uid)) {
return null;
}
return createWithLocationInfoSanitizedIfNecessaryWhenParceled(
@@ -2679,14 +2762,15 @@
private void redactUnderlyingNetworksForCapabilities(NetworkCapabilities nc, int pid, int uid) {
if (nc.getUnderlyingNetworks() != null
- && !checkNetworkFactoryOrSettingsPermission(pid, uid)) {
+ && !hasNetworkFactoryOrSettingsPermission(pid, uid)) {
nc.setUnderlyingNetworks(null);
}
}
private boolean canSeeAllowedUids(final int pid, final int uid, final int netOwnerUid) {
return Process.SYSTEM_UID == uid
- || checkAnyPermissionOf(mContext, pid, uid,
+ || netOwnerUid == uid
+ || hasAnyPermissionOf(mContext, pid, uid,
android.Manifest.permission.NETWORK_FACTORY);
}
@@ -2699,21 +2783,20 @@
// it happens for some reason (e.g. the package is uninstalled while CS is trying to
// send the callback) it would crash the system server with NPE.
final NetworkCapabilities newNc = new NetworkCapabilities(nc);
- if (!checkSettingsPermission(callerPid, callerUid)) {
+ if (!hasSettingsPermission(callerPid, callerUid)) {
newNc.setUids(null);
newNc.setSSID(null);
}
if (newNc.getNetworkSpecifier() != null) {
newNc.setNetworkSpecifier(newNc.getNetworkSpecifier().redact());
}
- if (!checkAnyPermissionOf(mContext, callerPid, callerUid,
+ if (!hasAnyPermissionOf(mContext, callerPid, callerUid,
android.Manifest.permission.NETWORK_STACK,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)) {
newNc.setAdministratorUids(new int[0]);
}
if (!canSeeAllowedUids(callerPid, callerUid, newNc.getOwnerUid())) {
newNc.setAllowedUids(new ArraySet<>());
- newNc.setSubscriptionIds(Collections.emptySet());
}
redactUnderlyingNetworksForCapabilities(newNc, callerPid, callerUid);
@@ -2774,11 +2857,12 @@
* Returns whether the app holds local mac address permission or not (might return cached
* result if the permission was already checked before).
*/
+ @CheckResult
public boolean hasLocalMacAddressPermission() {
if (mHasLocalMacAddressPermission == null) {
// If there is no cached result, perform the check now.
- mHasLocalMacAddressPermission =
- checkLocalMacAddressPermission(mCallingPid, mCallingUid);
+ mHasLocalMacAddressPermission = ConnectivityService.this
+ .hasLocalMacAddressPermission(mCallingPid, mCallingUid);
}
return mHasLocalMacAddressPermission;
}
@@ -2787,10 +2871,12 @@
* Returns whether the app holds settings permission or not (might return cached
* result if the permission was already checked before).
*/
+ @CheckResult
public boolean hasSettingsPermission() {
if (mHasSettingsPermission == null) {
// If there is no cached result, perform the check now.
- mHasSettingsPermission = checkSettingsPermission(mCallingPid, mCallingUid);
+ mHasSettingsPermission =
+ ConnectivityService.this.hasSettingsPermission(mCallingPid, mCallingUid);
}
return mHasSettingsPermission;
}
@@ -2894,7 +2980,7 @@
return new LinkProperties(lp);
}
- if (checkSettingsPermission(callerPid, callerUid)) {
+ if (hasSettingsPermission(callerPid, callerUid)) {
return new LinkProperties(lp, true /* parcelSensitiveFields */);
}
@@ -2910,7 +2996,7 @@
int callerUid, String callerPackageName) {
// There is no need to track the effective UID of the request here. If the caller
// lacks the settings permission, the effective UID is the same as the calling ID.
- if (!checkSettingsPermission()) {
+ if (!hasSettingsPermission()) {
// Unprivileged apps can only pass in null or their own UID.
if (nc.getUids() == null) {
// If the caller passes in null, the callback will also match networks that do not
@@ -2938,6 +3024,23 @@
}
}
+ private void maybeDisableLocalNetworkMatching(NetworkCapabilities nc, int callingUid) {
+ if (mDeps.isChangeEnabled(ENABLE_MATCH_LOCAL_NETWORK, callingUid)) {
+ return;
+ }
+ // If NET_CAPABILITY_LOCAL_NETWORK is not added to capability, request should not be
+ // satisfied by local networks.
+ if (!nc.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) {
+ nc.addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK);
+ }
+ }
+
+ private void restrictRequestNetworkCapabilitiesForCaller(NetworkCapabilities nc,
+ int callingUid, String callerPackageName) {
+ restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callerPackageName);
+ maybeDisableLocalNetworkMatching(nc, callingUid);
+ }
+
@Override
public @RestrictBackgroundStatus int getRestrictBackgroundStatusByCaller() {
enforceAccessPermission();
@@ -3021,26 +3124,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.
@@ -3056,7 +3139,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 */);
@@ -3369,7 +3453,8 @@
public static final String ALLOW_SYSUI_CONNECTIVITY_REPORTS =
"allow_sysui_connectivity_reports";
- public static final String LOG_BPF_RC = "log_bpf_rc_force_disable";
+ public static final String ALLOW_SATALLITE_NETWORK_FALLBACK =
+ "allow_satallite_network_fallback";
private void enforceInternetPermission() {
mContext.enforceCallingOrSelfPermission(
@@ -3383,7 +3468,8 @@
"ConnectivityService");
}
- private boolean checkAccessPermission(int pid, int uid) {
+ @CheckResult
+ private boolean hasAccessPermission(int pid, int uid) {
return mContext.checkPermission(android.Manifest.permission.ACCESS_NETWORK_STATE, pid, uid)
== PERMISSION_GRANTED;
}
@@ -3469,7 +3555,8 @@
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
}
- private boolean checkNetworkFactoryOrSettingsPermission(int pid, int uid) {
+ @CheckResult
+ private boolean hasNetworkFactoryOrSettingsPermission(int pid, int uid) {
return PERMISSION_GRANTED == mContext.checkPermission(
android.Manifest.permission.NETWORK_FACTORY, pid, uid)
|| PERMISSION_GRANTED == mContext.checkPermission(
@@ -3479,13 +3566,14 @@
|| UserHandle.getAppId(uid) == Process.BLUETOOTH_UID;
}
- private boolean checkSettingsPermission() {
- return PermissionUtils.checkAnyPermissionOf(mContext,
- android.Manifest.permission.NETWORK_SETTINGS,
+ @CheckResult
+ private boolean hasSettingsPermission() {
+ return hasAnyPermissionOf(mContext, android.Manifest.permission.NETWORK_SETTINGS,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
}
- private boolean checkSettingsPermission(int pid, int uid) {
+ @CheckResult
+ private boolean hasSettingsPermission(int pid, int uid) {
return PERMISSION_GRANTED == mContext.checkPermission(
android.Manifest.permission.NETWORK_SETTINGS, pid, uid)
|| PERMISSION_GRANTED == mContext.checkPermission(
@@ -3522,33 +3610,36 @@
"ConnectivityService");
}
- private boolean checkNetworkStackPermission() {
- return PermissionUtils.checkAnyPermissionOf(mContext,
- android.Manifest.permission.NETWORK_STACK,
+ @CheckResult
+ private boolean hasNetworkStackPermission() {
+ return hasAnyPermissionOf(mContext, android.Manifest.permission.NETWORK_STACK,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
}
- private boolean checkNetworkStackPermission(int pid, int uid) {
- return checkAnyPermissionOf(mContext, pid, uid,
- android.Manifest.permission.NETWORK_STACK,
+ @CheckResult
+ private boolean hasNetworkStackPermission(int pid, int uid) {
+ return hasAnyPermissionOf(mContext, pid, uid, android.Manifest.permission.NETWORK_STACK,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
}
- private boolean checkSystemBarServicePermission(int pid, int uid) {
- return checkAnyPermissionOf(mContext, pid, uid,
+ @CheckResult
+ private boolean hasSystemBarServicePermission(int pid, int uid) {
+ return hasAnyPermissionOf(mContext, pid, uid,
android.Manifest.permission.STATUS_BAR_SERVICE);
}
- private boolean checkNetworkSignalStrengthWakeupPermission(int pid, int uid) {
- return checkAnyPermissionOf(mContext, pid, uid,
+ @CheckResult
+ private boolean hasNetworkSignalStrengthWakeupPermission(int pid, int uid) {
+ return hasAnyPermissionOf(mContext, pid, uid,
android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
android.Manifest.permission.NETWORK_SETTINGS);
}
- private boolean checkConnectivityRestrictedNetworksPermission(int callingUid,
+ @CheckResult
+ private boolean hasConnectivityRestrictedNetworksPermission(int callingUid,
boolean checkUidsAllowedList) {
- if (PermissionUtils.checkAnyPermissionOf(mContext,
+ if (hasAnyPermissionOf(mContext,
android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS)) {
return true;
}
@@ -3556,8 +3647,7 @@
// fallback to ConnectivityInternalPermission
// TODO: Remove this fallback check after all apps have declared
// CONNECTIVITY_USE_RESTRICTED_NETWORKS.
- if (PermissionUtils.checkAnyPermissionOf(mContext,
- android.Manifest.permission.CONNECTIVITY_INTERNAL)) {
+ if (hasAnyPermissionOf(mContext, android.Manifest.permission.CONNECTIVITY_INTERNAL)) {
return true;
}
@@ -3571,7 +3661,7 @@
private void enforceConnectivityRestrictedNetworksPermission(boolean checkUidsAllowedList) {
final int callingUid = mDeps.getCallingUid();
- if (!checkConnectivityRestrictedNetworksPermission(callingUid, checkUidsAllowedList)) {
+ if (!hasConnectivityRestrictedNetworksPermission(callingUid, checkUidsAllowedList)) {
throw new SecurityException("ConnectivityService: user " + callingUid
+ " has no permission to access restricted network.");
}
@@ -3581,7 +3671,8 @@
mContext.enforceCallingOrSelfPermission(KeepaliveTracker.PERMISSION, "ConnectivityService");
}
- private boolean checkLocalMacAddressPermission(int pid, int uid) {
+ @CheckResult
+ private boolean hasLocalMacAddressPermission(int pid, int uid) {
return PERMISSION_GRANTED == mContext.checkPermission(
Manifest.permission.LOCAL_MAC_ADDRESS, pid, uid);
}
@@ -3731,6 +3822,14 @@
updateMobileDataPreferredUids();
}
+ if (mSatelliteAccessController != null) {
+ mSatelliteAccessController.start();
+ }
+
+ if (mCarrierPrivilegeAuthenticator != null) {
+ mCarrierPrivilegeAuthenticator.start();
+ }
+
// On T+ devices, register callback for statsd to pull NETWORK_BPF_MAP_INFO atom
if (mDeps.isAtLeastT()) {
mBpfNetMaps.setPullAtomCallback(mContext);
@@ -3875,12 +3974,13 @@
@Override
protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
@Nullable String[] args) {
- if (!checkDumpPermission(mContext, TAG, writer)) return;
+ if (!hasDumpPermission(mContext, TAG, writer)) return;
mPriorityDumper.dump(fd, writer, args);
}
- private boolean checkDumpPermission(Context context, String tag, PrintWriter pw) {
+ @CheckResult
+ private boolean hasDumpPermission(Context context, String tag, PrintWriter pw) {
if (context.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
!= PackageManager.PERMISSION_GRANTED) {
pw.println("Permission Denial: can't dump " + tag + " from from pid="
@@ -4051,6 +4151,13 @@
pw.increaseIndent();
mNetworkActivityTracker.dump(pw);
pw.decreaseIndent();
+
+ pw.println();
+ pw.println("Multicast routing supported: " +
+ (mMulticastRoutingCoordinatorService != null));
+
+ pw.println();
+ pw.println("Background firewall chain enabled: " + mBackgroundFirewallChainEnabled);
}
private void dumpNetworks(IndentingPrintWriter pw) {
@@ -5192,9 +5299,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 "
@@ -5244,6 +5354,7 @@
// was is being disconnected the callbacks have already been sent, and if it is being
// destroyed pending replacement they will be sent when it is disconnected.
maybeDisableForwardRulesForDisconnectingNai(nai, false /* sendCallbacks */);
+ updateIngressToVpnAddressFiltering(null, nai.linkProperties, nai);
try {
mNetd.networkDestroy(nai.network.getNetId());
} catch (RemoteException | ServiceSpecificException e) {
@@ -5305,6 +5416,13 @@
return false;
}
+ private int getSubscriptionIdFromNetworkCaps(@NonNull final NetworkCapabilities caps) {
+ if (mCarrierPrivilegeAuthenticator != null) {
+ return mCarrierPrivilegeAuthenticator.getSubIdFromNetworkCapabilities(caps);
+ }
+ return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ }
+
private void handleRegisterNetworkRequestWithIntent(@NonNull final Message msg) {
final NetworkRequestInfo nri = (NetworkRequestInfo) (msg.obj);
// handleRegisterNetworkRequestWithIntent() doesn't apply to multilayer requests.
@@ -5690,7 +5808,7 @@
}
private RequestInfoPerUidCounter getRequestCounter(NetworkRequestInfo nri) {
- return checkAnyPermissionOf(mContext,
+ return hasAnyPermissionOf(mContext,
nri.mPid, nri.mUid, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
? mSystemNetworkRequestCounter : mNetworkRequestCounter;
}
@@ -5914,7 +6032,15 @@
if (nm == null) return;
if (request == CaptivePortal.APP_REQUEST_REEVALUATION_REQUIRED) {
- checkNetworkStackPermission();
+ // This enforceNetworkStackPermission() should be adopted to check
+ // the required permission but this may be break OEM captive portal
+ // apps. Simply ignore the request if the caller does not have
+ // permission.
+ if (!hasNetworkStackPermission()) {
+ Log.e(TAG, "Calling appRequest() without proper permission. Skip");
+ return;
+ }
+
nm.forceReevaluation(mDeps.getCallingUid());
}
}
@@ -5944,7 +6070,7 @@
* @see MultinetworkPolicyTracker#getAvoidBadWifi()
*/
public boolean shouldAvoidBadWifi() {
- if (!checkNetworkStackPermission()) {
+ if (!hasNetworkStackPermission()) {
throw new SecurityException("avoidBadWifi requires NETWORK_STACK permission");
}
return avoidBadWifi();
@@ -6313,10 +6439,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);
@@ -7466,20 +7590,16 @@
// specific SSID/SignalStrength, or the calling app has permission to do so.
private void ensureSufficientPermissionsForRequest(NetworkCapabilities nc,
int callerPid, int callerUid, String callerPackageName) {
- if (null != nc.getSsid() && !checkSettingsPermission(callerPid, callerUid)) {
+ if (null != nc.getSsid() && !hasSettingsPermission(callerPid, callerUid)) {
throw new SecurityException("Insufficient permissions to request a specific SSID");
}
if (nc.hasSignalStrength()
- && !checkNetworkSignalStrengthWakeupPermission(callerPid, callerUid)) {
+ && !hasNetworkSignalStrengthWakeupPermission(callerPid, callerUid)) {
throw new SecurityException(
"Insufficient permissions to request a specific signal strength");
}
mAppOpsManager.checkPackage(callerUid, callerPackageName);
-
- if (!nc.getSubscriptionIds().isEmpty()) {
- enforceNetworkFactoryPermission();
- }
}
private int[] getSignalStrengthThresholds(@NonNull final NetworkAgentInfo nai) {
@@ -7569,7 +7689,7 @@
int reqTypeInt, Messenger messenger, int timeoutMs, final IBinder binder,
int legacyType, int callbackFlags, @NonNull String callingPackageName,
@Nullable String callingAttributionTag) {
- if (legacyType != TYPE_NONE && !checkNetworkStackPermission()) {
+ if (legacyType != TYPE_NONE && !hasNetworkStackPermission()) {
if (isTargetSdkAtleast(Build.VERSION_CODES.M, mDeps.getCallingUid(),
callingPackageName)) {
throw new SecurityException("Insufficient permissions to specify legacy type");
@@ -7613,10 +7733,12 @@
// the state of the app when the request is filed, but we never change the
// request if the app changes network state. http://b/29964605
enforceMeteredApnPolicy(networkCapabilities);
+ maybeDisableLocalNetworkMatching(networkCapabilities, callingUid);
break;
case LISTEN_FOR_BEST:
enforceAccessPermission();
networkCapabilities = new NetworkCapabilities(networkCapabilities);
+ maybeDisableLocalNetworkMatching(networkCapabilities, callingUid);
break;
default:
throw new IllegalArgumentException("Unsupported request type " + reqType);
@@ -7703,7 +7825,7 @@
final UserHandle user = UserHandle.getUserHandleForUid(callingUid);
// Only run the check if the change is enabled.
if (!mDeps.isChangeEnabled(
- ConnectivityCompatChanges.ENABLE_SELF_CERTIFIED_CAPABILITIES_DECLARATION,
+ ENABLE_SELF_CERTIFIED_CAPABILITIES_DECLARATION,
callingPackageName, user)) {
return false;
}
@@ -7759,6 +7881,22 @@
applicationNetworkCapabilities.enforceSelfCertifiedNetworkCapabilitiesDeclared(
networkCapabilities);
}
+
+ private boolean canRequestRestrictedNetworkDueToCarrierPrivileges(
+ NetworkCapabilities networkCapabilities, int callingUid) {
+ if (mRequestRestrictedWifiEnabled) {
+ // For U+ devices, callers with carrier privilege could request restricted networks
+ // with CBS capabilities, or any restricted WiFi networks.
+ return ((networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)
+ || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
+ && hasCarrierPrivilegeForNetworkCaps(callingUid, networkCapabilities));
+ } else {
+ // For T+ devices, callers with carrier privilege could request with CBS
+ // capabilities.
+ return (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)
+ && hasCarrierPrivilegeForNetworkCaps(callingUid, networkCapabilities));
+ }
+ }
private void enforceNetworkRequestPermissions(NetworkCapabilities networkCapabilities,
String callingPackageName, String callingAttributionTag, final int callingUid) {
if (shouldCheckCapabilitiesDeclaration(networkCapabilities, callingUid,
@@ -7766,13 +7904,11 @@
enforceRequestCapabilitiesDeclaration(callingPackageName, networkCapabilities,
callingUid);
}
- if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) == false) {
- // For T+ devices, callers with carrier privilege could request with CBS capabilities.
- if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)
- && hasCarrierPrivilegeForNetworkCaps(callingUid, networkCapabilities)) {
- return;
+ if (!networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
+ if (!canRequestRestrictedNetworkDueToCarrierPrivileges(
+ networkCapabilities, callingUid)) {
+ enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
}
- enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
} else {
enforceChangePermission(callingPackageName, callingAttributionTag);
}
@@ -7841,8 +7977,8 @@
ensureRequestableCapabilities(networkCapabilities);
ensureSufficientPermissionsForRequest(networkCapabilities,
Binder.getCallingPid(), callingUid, callingPackageName);
- restrictRequestUidsForCallerAndSetRequestorInfo(networkCapabilities,
- callingUid, callingPackageName);
+ restrictRequestNetworkCapabilitiesForCaller(
+ networkCapabilities, callingUid, callingPackageName);
NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, TYPE_NONE,
nextNetworkRequestId(), NetworkRequest.Type.REQUEST);
@@ -7902,7 +8038,7 @@
NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
ensureSufficientPermissionsForRequest(networkCapabilities,
Binder.getCallingPid(), callingUid, callingPackageName);
- restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName);
+ restrictRequestNetworkCapabilitiesForCaller(nc, callingUid, callingPackageName);
// Apps without the CHANGE_NETWORK_STATE permission can't use background networks, so
// make all their listens include NET_CAPABILITY_FOREGROUND. That way, they will get
// onLost and onAvailable callbacks when networks move in and out of the background.
@@ -7935,7 +8071,7 @@
ensureSufficientPermissionsForRequest(networkCapabilities,
Binder.getCallingPid(), callingUid, callingPackageName);
final NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
- restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName);
+ restrictRequestNetworkCapabilitiesForCaller(nc, callingUid, callingPackageName);
NetworkRequest networkRequest = new NetworkRequest(nc, TYPE_NONE, nextNetworkRequestId(),
NetworkRequest.Type.LISTEN);
@@ -8566,6 +8702,8 @@
// new interface (the interface name -> index map becomes initialized)
updateVpnFiltering(newLp, oldLp, networkAgent);
+ updateIngressToVpnAddressFiltering(newLp, oldLp, networkAgent);
+
updateMtu(newLp, oldLp);
// TODO - figure out what to do for clat
// for (LinkProperties lp : newLp.getStackedLinks()) {
@@ -8857,6 +8995,87 @@
}
}
+ /**
+ * Returns ingress discard rules to drop packets to VPN addresses ingressing via non-VPN
+ * interfaces.
+ * Ingress discard rule is added to the address iff
+ * 1. The address is not a link local address
+ * 2. The address is used by a single VPN interface and not used by any other
+ * interfaces even non-VPN ones
+ * This method can be called during network disconnects, when nai has already been removed from
+ * mNetworkAgentInfos.
+ *
+ * @param nai This method generates rules assuming lp of this nai is the lp at the second
+ * argument.
+ * @param lp This method generates rules assuming lp of nai at the first argument is this lp.
+ * Caller passes old lp to generate old rules and new lp to generate new rules.
+ * @return ingress discard rules. Set of pairs of addresses and interface names
+ */
+ private Set<Pair<InetAddress, String>> generateIngressDiscardRules(
+ @NonNull final NetworkAgentInfo nai, @Nullable final LinkProperties lp) {
+ Set<NetworkAgentInfo> nais = new ArraySet<>(mNetworkAgentInfos);
+ nais.add(nai);
+ // Determine how many networks each IP address is currently configured on.
+ // Ingress rules are added only for IP addresses that are configured on single interface.
+ final Map<InetAddress, Integer> addressOwnerCounts = new ArrayMap<>();
+ for (final NetworkAgentInfo agent : nais) {
+ if (agent.isDestroyed()) {
+ continue;
+ }
+ final LinkProperties agentLp = (nai == agent) ? lp : agent.linkProperties;
+ if (agentLp == null) {
+ continue;
+ }
+ for (final InetAddress addr: agentLp.getAllAddresses()) {
+ addressOwnerCounts.put(addr, addressOwnerCounts.getOrDefault(addr, 0) + 1);
+ }
+ }
+
+ // Iterates all networks instead of only generating rule for nai that was passed in since
+ // lp of the nai change could cause/resolve address collision and result in affecting rule
+ // for different network.
+ final Set<Pair<InetAddress, String>> ingressDiscardRules = new ArraySet<>();
+ for (final NetworkAgentInfo agent : nais) {
+ if (!agent.isVPN() || agent.isDestroyed()) {
+ continue;
+ }
+ final LinkProperties agentLp = (nai == agent) ? lp : agent.linkProperties;
+ if (agentLp == null || agentLp.getInterfaceName() == null) {
+ continue;
+ }
+
+ for (final InetAddress addr: agentLp.getAllAddresses()) {
+ if (addressOwnerCounts.get(addr) == 1 && !addr.isLinkLocalAddress()) {
+ ingressDiscardRules.add(new Pair<>(addr, agentLp.getInterfaceName()));
+ }
+ }
+ }
+ return ingressDiscardRules;
+ }
+
+ private void updateIngressToVpnAddressFiltering(@Nullable LinkProperties newLp,
+ @Nullable LinkProperties oldLp, @NonNull NetworkAgentInfo nai) {
+ // Having isAtleastT to avoid NewApi linter error (b/303382209)
+ if (!mIngressToVpnAddressFiltering || !mDeps.isAtLeastT()) {
+ return;
+ }
+ final CompareOrUpdateResult<InetAddress, Pair<InetAddress, String>> ruleDiff =
+ new CompareOrUpdateResult<>(
+ generateIngressDiscardRules(nai, oldLp),
+ generateIngressDiscardRules(nai, newLp),
+ (rule) -> rule.first);
+ for (Pair<InetAddress, String> rule: ruleDiff.removed) {
+ mBpfNetMaps.removeIngressDiscardRule(rule.first);
+ }
+ for (Pair<InetAddress, String> rule: ruleDiff.added) {
+ mBpfNetMaps.setIngressDiscardRule(rule.first, rule.second);
+ }
+ // setIngressDiscardRule overrides the existing rule
+ for (Pair<InetAddress, String> rule: ruleDiff.updated) {
+ mBpfNetMaps.setIngressDiscardRule(rule.first, rule.second);
+ }
+ }
+
private void updateWakeOnLan(@NonNull LinkProperties lp) {
if (mWolSupportedInterfaces == null) {
mWolSupportedInterfaces = new ArraySet<>(mResources.get().getStringArray(
@@ -9039,6 +9258,43 @@
}
}
+ private void handleUidCarrierPrivilegesLost(int uid, int subId) {
+ if (!mRequestRestrictedWifiEnabled) {
+ return;
+ }
+ ensureRunningOnConnectivityServiceThread();
+ // A NetworkRequest needs to be revoked when all the conditions are met
+ // 1. It requests restricted network
+ // 2. The requestor uid matches the uid with the callback
+ // 3. The app doesn't have Carrier Privileges
+ // 4. The app doesn't have permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS
+ for (final NetworkRequest nr : mNetworkRequests.keySet()) {
+ if (nr.isRequest()
+ && !nr.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ && nr.getRequestorUid() == uid
+ && getSubscriptionIdFromNetworkCaps(nr.networkCapabilities) == subId
+ && !hasConnectivityRestrictedNetworksPermission(uid, true)) {
+ declareNetworkRequestUnfulfillable(nr);
+ }
+ }
+
+ // A NetworkAgent's allowedUids may need to be updated if the app has lost
+ // carrier config
+ for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+ if (nai.networkCapabilities.getAllowedUidsNoCopy().contains(uid)
+ && getSubscriptionIdFromNetworkCaps(nai.networkCapabilities) == subId) {
+ final NetworkCapabilities nc = new NetworkCapabilities(nai.networkCapabilities);
+ NetworkAgentInfo.restrictCapabilitiesFromNetworkAgent(
+ nc,
+ uid,
+ false /* hasAutomotiveFeature (irrelevant) */,
+ mDeps,
+ mCarrierPrivilegeAuthenticator);
+ updateCapabilities(nai.getScore(), nai, nc);
+ }
+ }
+ }
+
/**
* Update the NetworkCapabilities for {@code nai} to {@code nc}. Specifically:
*
@@ -9116,6 +9372,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,
@@ -9129,7 +9450,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(
@@ -9188,6 +9508,7 @@
configBuilder.setUpstreamSelector(oldRequest);
nai.localNetworkConfig = configBuilder.build();
}
+ maybeApplyMulticastRoutingConfig(nai, oldConfig, newConfig);
}
/**
@@ -9421,7 +9742,6 @@
final ArraySet<Integer> toAdd = new ArraySet<>(newUids);
toRemove.removeAll(newUids);
toAdd.removeAll(prevUids);
-
try {
if (!toAdd.isEmpty()) {
mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig(
@@ -10187,6 +10507,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 !)
@@ -10195,6 +10517,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);
@@ -11062,17 +11387,28 @@
err.getFileDescriptor(), args);
}
- private Boolean parseBooleanArgument(final String arg) {
- if ("true".equals(arg)) {
- return true;
- } else if ("false".equals(arg)) {
- return false;
- } else {
- return null;
- }
- }
-
private class ShellCmd extends BasicShellCommandHandler {
+
+ private Boolean parseBooleanArgument(final String arg) {
+ if ("true".equals(arg)) {
+ return true;
+ } else if ("false".equals(arg)) {
+ return false;
+ } else {
+ getOutPrintWriter().println("Invalid boolean argument: " + arg);
+ return null;
+ }
+ }
+
+ private Integer parseIntegerArgument(final String arg) {
+ try {
+ return Integer.valueOf(arg);
+ } catch (NumberFormatException ne) {
+ getOutPrintWriter().println("Invalid integer argument: " + arg);
+ return null;
+ }
+ }
+
@Override
public int onCommand(String cmd) {
if (cmd == null) {
@@ -11149,6 +11485,38 @@
}
return 0;
}
+ case "set-background-networking-enabled-for-uid": {
+ final Integer uid = parseIntegerArgument(getNextArg());
+ final Boolean enabled = parseBooleanArgument(getNextArg());
+ if (null == enabled || null == uid) {
+ onHelp();
+ return -1;
+ }
+ final int rule = enabled ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DEFAULT;
+ setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, uid, rule);
+ final String msg = (enabled ? "Enabled" : "Disabled")
+ + " background networking for uid " + uid;
+ Log.i(TAG, msg);
+ pw.println(msg);
+ return 0;
+ }
+ case "get-background-networking-enabled-for-uid": {
+ final Integer uid = parseIntegerArgument(getNextArg());
+ if (null == uid) {
+ onHelp();
+ return -1;
+ }
+ final int rule = getUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, uid);
+ if (FIREWALL_RULE_ALLOW == rule) {
+ pw.println(uid + ": allow");
+ } else if (FIREWALL_RULE_DENY == rule || FIREWALL_RULE_DEFAULT == rule) {
+ pw.println(uid + ": deny");
+ } else {
+ throw new IllegalStateException(
+ "Unknown rule " + rule + " for uid " + uid);
+ }
+ return 0;
+ }
case "reevaluate":
// Usage : adb shell cmd connectivity reevaluate <netId>
// If netId is omitted, then reevaluate the default network
@@ -11209,6 +11577,10 @@
+ " no effect if the chain is disabled.");
pw.println(" get-package-networking-enabled [package name]");
pw.println(" Get the deny bit in FIREWALL_CHAIN_OEM_DENY_3 for package.");
+ pw.println(" set-background-networking-enabled-for-uid [uid] [true|false]");
+ pw.println(" Set the allow bit in FIREWALL_CHAIN_BACKGROUND for the given uid.");
+ pw.println(" get-background-networking-enabled-for-uid [uid]");
+ pw.println(" Get the allow bit in FIREWALL_CHAIN_BACKGROUND for the given uid.");
}
}
@@ -11249,7 +11621,7 @@
// Connection owner UIDs are visible only to the network stack and to the VpnService-based
// VPN, if any, that applies to the UID that owns the connection.
- if (checkNetworkStackPermission()) return uid;
+ if (hasNetworkStackPermission()) return uid;
final NetworkAgentInfo vpn = getVpnForUid(uid);
if (vpn == null || getVpnType(vpn) != VpnManager.TYPE_VPN_SERVICE
@@ -11509,7 +11881,7 @@
if (report == null) {
continue;
}
- if (!checkConnectivityDiagnosticsPermissions(
+ if (!hasConnectivityDiagnosticsPermissions(
nri.mPid, nri.mUid, nai, cbInfo.mCallingPackageName)) {
continue;
}
@@ -11672,7 +12044,7 @@
continue;
}
- if (!checkConnectivityDiagnosticsPermissions(
+ if (!hasConnectivityDiagnosticsPermissions(
nri.mPid, nri.mUid, nai, cbInfo.mCallingPackageName)) {
continue;
}
@@ -11716,14 +12088,15 @@
return false;
}
+ @CheckResult
@VisibleForTesting
- boolean checkConnectivityDiagnosticsPermissions(
+ boolean hasConnectivityDiagnosticsPermissions(
int callbackPid, int callbackUid, NetworkAgentInfo nai, String callbackPackageName) {
- if (checkNetworkStackPermission(callbackPid, callbackUid)) {
+ if (hasNetworkStackPermission(callbackPid, callbackUid)) {
return true;
}
if (mAllowSysUiConnectivityReports
- && checkSystemBarServicePermission(callbackPid, callbackUid)) {
+ && hasSystemBarServicePermission(callbackPid, callbackUid)) {
return true;
}
@@ -11757,7 +12130,7 @@
// This NetworkCapabilities is only used for matching to Networks. Clear out its owner uid
// and administrator uids to be safe.
final NetworkCapabilities nc = new NetworkCapabilities(request.networkCapabilities);
- restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName);
+ restrictRequestNetworkCapabilitiesForCaller(nc, callingUid, callingPackageName);
final NetworkRequest requestWithId =
new NetworkRequest(
@@ -12589,16 +12962,27 @@
@VisibleForTesting
@NonNull
- ArraySet<NetworkRequestInfo> createNrisFromMobileDataPreferredUids(
- @NonNull final Set<Integer> uids) {
+ ArraySet<NetworkRequestInfo> createNrisForPreferenceOrder(@NonNull final Set<Integer> uids,
+ @NonNull final List<NetworkRequest> requests,
+ final int preferenceOrder) {
final ArraySet<NetworkRequestInfo> nris = new ArraySet<>();
if (uids.size() == 0) {
// Should not create NetworkRequestInfo if no preferences. Without uid range in
// NetworkRequestInfo, makeDefaultForApps() would treat it as a illegal NRI.
- if (DBG) log("Don't create NetworkRequestInfo because no preferences");
return nris;
}
+ final Set<UidRange> ranges = new ArraySet<>();
+ for (final int uid : uids) {
+ ranges.add(new UidRange(uid, uid));
+ }
+ setNetworkRequestUids(requests, ranges);
+ nris.add(new NetworkRequestInfo(Process.myUid(), requests, preferenceOrder));
+ return nris;
+ }
+
+ ArraySet<NetworkRequestInfo> createNrisFromMobileDataPreferredUids(
+ @NonNull final Set<Integer> uids) {
final List<NetworkRequest> requests = new ArrayList<>();
// The NRI should be comprised of two layers:
// - The request for the mobile network preferred.
@@ -12607,14 +12991,28 @@
TRANSPORT_CELLULAR, NetworkRequest.Type.REQUEST));
requests.add(createDefaultInternetRequestForTransport(
TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
- final Set<UidRange> ranges = new ArraySet<>();
- for (final int uid : uids) {
- ranges.add(new UidRange(uid, uid));
- }
- setNetworkRequestUids(requests, ranges);
- nris.add(new NetworkRequestInfo(Process.myUid(), requests,
- PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED));
- return nris;
+ return createNrisForPreferenceOrder(uids, requests, PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED
+ );
+ }
+
+ ArraySet<NetworkRequestInfo> createMultiLayerNrisFromSatelliteNetworkFallbackUids(
+ @NonNull final Set<Integer> uids) {
+ final List<NetworkRequest> requests = new ArrayList<>();
+
+ // request: track default(unrestricted internet network)
+ requests.add(createDefaultInternetRequestForTransport(
+ TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
+
+ // request: restricted Satellite internet
+ final NetworkCapabilities cap = new NetworkCapabilities.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ .addTransportType(NetworkCapabilities.TRANSPORT_SATELLITE)
+ .build();
+ requests.add(createNetworkRequest(NetworkRequest.Type.REQUEST, cap));
+
+ return createNrisForPreferenceOrder(uids, requests, PREFERENCE_ORDER_SATELLITE_FALLBACK);
}
private void handleMobileDataPreferredUidsChanged() {
@@ -12626,6 +13024,16 @@
rematchAllNetworksAndRequests();
}
+ private void handleSetSatelliteNetworkPreference(
+ @NonNull final Set<Integer> satelliteNetworkPreferredUids) {
+ removeDefaultNetworkRequestsForPreference(PREFERENCE_ORDER_SATELLITE_FALLBACK);
+ addPerAppDefaultNetworkRequests(
+ createMultiLayerNrisFromSatelliteNetworkFallbackUids(satelliteNetworkPreferredUids)
+ );
+ // Finally, rematch.
+ rematchAllNetworksAndRequests();
+ }
+
private void handleIngressRateLimitChanged() {
final long oldIngressRateLimit = mIngressRateLimit;
mIngressRateLimit = ConnectivitySettingsManager.getIngressRateLimitInBytesPerSecond(
@@ -13119,6 +13527,12 @@
public void setUidFirewallRule(final int chain, final int uid, final int rule) {
enforceNetworkStackOrSettingsPermission();
+ if (chain == FIREWALL_CHAIN_BACKGROUND && !mBackgroundFirewallChainEnabled) {
+ Log.i(TAG, "Ignoring operation setUidFirewallRule on the background chain because the"
+ + " feature is disabled.");
+ return;
+ }
+
// There are only two type of firewall rule: FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
int firewallRule = getFirewallRuleType(chain, rule);
@@ -13191,6 +13605,12 @@
public void setFirewallChainEnabled(final int chain, final boolean enable) {
enforceNetworkStackOrSettingsPermission();
+ if (chain == FIREWALL_CHAIN_BACKGROUND && !mBackgroundFirewallChainEnabled) {
+ Log.i(TAG, "Ignoring operation setFirewallChainEnabled on the background chain because"
+ + " the feature is disabled.");
+ return;
+ }
+
try {
mBpfNetMaps.setChildChain(chain, enable);
} catch (ServiceSpecificException e) {
@@ -13217,6 +13637,12 @@
public void replaceFirewallChain(final int chain, final int[] uids) {
enforceNetworkStackOrSettingsPermission();
+ if (chain == FIREWALL_CHAIN_BACKGROUND && !mBackgroundFirewallChainEnabled) {
+ Log.i(TAG, "Ignoring operation replaceFirewallChain on the background chain because"
+ + " the feature is disabled.");
+ return;
+ }
+
mBpfNetMaps.replaceUidChain(chain, uids);
}
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index 94ba9de..31108fc 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -50,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;
@@ -75,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.
@@ -373,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) {
@@ -745,7 +742,7 @@
}
@VisibleForTesting
- boolean isAnyTcpSocketConnected(int netId, @NonNull Set<Range<Integer>> vpnUidRanges) {
+ boolean isAnyTcpSocketConnected(int netId) {
FileDescriptor fd = null;
try {
@@ -758,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;
}
}
@@ -773,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.
@@ -802,7 +798,7 @@
}
final InetDiagMessage diagMsg = (InetDiagMessage) nlMsg;
- if (isTargetTcpSocket(diagMsg, networkMark, networkMask, vpnUidRanges)) {
+ if (isTargetTcpSocket(diagMsg, networkMark, networkMask)) {
if (DBG) {
Log.d(TAG, String.format("Found open TCP connection by uid %d to %s"
+ " cookie %d",
@@ -828,19 +824,8 @@
return false;
}
- private static boolean containsUid(Set<Range<Integer>> ranges, int uid) {
- for (final Range<Integer> range: ranges) {
- if (range.contains(uid)) {
- return true;
- }
- }
- return false;
- }
-
private boolean isTargetTcpSocket(@NonNull InetDiagMessage diagMsg,
- int networkMark, int networkMask, @NonNull Set<Range<Integer>> vpnUidRanges) {
- if (!containsUid(vpnUidRanges, diagMsg.inetDiagMsg.idiag_uid)) return false;
-
+ int networkMark, int networkMask) {
final int mark = readSocketDataAndReturnMark(diagMsg);
return (mark & networkMask) == networkMark;
}
diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
index 5705ebe..f5fa4fb 100644
--- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
+++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -40,12 +40,13 @@
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.Log;
-import android.util.SparseIntArray;
+import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.modules.utils.HandlerExecutor;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.DeviceConfigUtils;
import com.android.networkstack.apishim.TelephonyManagerShimImpl;
import com.android.networkstack.apishim.common.TelephonyManagerShim;
@@ -55,6 +56,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
/**
* Tracks the uid of the carrier privileged app that provides the carrier config.
@@ -71,7 +73,8 @@
private final TelephonyManagerShim mTelephonyManagerShim;
private final TelephonyManager mTelephonyManager;
@GuardedBy("mLock")
- private final SparseIntArray mCarrierServiceUid = new SparseIntArray(2 /* initialCapacity */);
+ private final SparseArray<CarrierServiceUidWithSubId> mCarrierServiceUidWithSubId =
+ new SparseArray<>(2 /* initialCapacity */);
@GuardedBy("mLock")
private int mModemCount = 0;
private final Object mLock = new Object();
@@ -79,42 +82,71 @@
@NonNull
private final List<PrivilegeListener> mCarrierPrivilegesChangedListeners = new ArrayList<>();
private final boolean mUseCallbacksForServiceChanged;
+ private final boolean mRequestRestrictedWifiEnabled;
+ @NonNull
+ private final BiConsumer<Integer, Integer> mListener;
public CarrierPrivilegeAuthenticator(@NonNull final Context c,
@NonNull final Dependencies deps,
@NonNull final TelephonyManager t,
- @NonNull final TelephonyManagerShim telephonyManagerShim) {
+ @NonNull final TelephonyManagerShim telephonyManagerShim,
+ final boolean requestRestrictedWifiEnabled,
+ @NonNull BiConsumer<Integer, Integer> listener,
+ @NonNull final Handler connectivityServiceHandler) {
mContext = c;
mTelephonyManager = t;
mTelephonyManagerShim = telephonyManagerShim;
- final HandlerThread thread = deps.makeHandlerThread();
- thread.start();
- mHandler = new Handler(thread.getLooper());
mUseCallbacksForServiceChanged = deps.isFeatureEnabled(
c, CARRIER_SERVICE_CHANGED_USE_CALLBACK);
+ mRequestRestrictedWifiEnabled = requestRestrictedWifiEnabled;
+ mListener = listener;
+ if (mRequestRestrictedWifiEnabled) {
+ mHandler = connectivityServiceHandler;
+ } else {
+ final HandlerThread thread = deps.makeHandlerThread();
+ thread.start();
+ mHandler = new Handler(thread.getLooper());
+ synchronized (mLock) {
+ registerSimConfigChangedReceiver();
+ simConfigChanged();
+ }
+ }
+ }
+
+ private void registerSimConfigChangedReceiver() {
final IntentFilter filter = new IntentFilter();
filter.addAction(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED);
- synchronized (mLock) {
- // Never unregistered because the system server never stops
- c.registerReceiver(new BroadcastReceiver() {
- @Override
- public void onReceive(final Context context, final Intent intent) {
- switch (intent.getAction()) {
- case TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED:
- simConfigChanged();
- break;
- default:
- Log.d(TAG, "Unknown intent received, action: " + intent.getAction());
- }
+ // Never unregistered because the system server never stops
+ mContext.registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ switch (intent.getAction()) {
+ case TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED:
+ simConfigChanged();
+ break;
+ default:
+ Log.d(TAG, "Unknown intent received, action: " + intent.getAction());
}
- }, filter, null, mHandler);
- simConfigChanged();
+ }
+ }, filter, null, mHandler);
+ }
+
+ /**
+ * Start CarrierPrivilegeAuthenticator
+ */
+ public void start() {
+ if (mRequestRestrictedWifiEnabled) {
+ registerSimConfigChangedReceiver();
+ mHandler.post(this::simConfigChanged);
}
}
public CarrierPrivilegeAuthenticator(@NonNull final Context c,
- @NonNull final TelephonyManager t) {
- this(c, new Dependencies(), t, TelephonyManagerShimImpl.newInstance(t));
+ @NonNull final TelephonyManager t, final boolean requestRestrictedWifiEnabled,
+ @NonNull BiConsumer<Integer, Integer> listener,
+ @NonNull final Handler connectivityServiceHandler) {
+ this(c, new Dependencies(), t, TelephonyManagerShimImpl.newInstance(t),
+ requestRestrictedWifiEnabled, listener, connectivityServiceHandler);
}
public static class Dependencies {
@@ -134,6 +166,10 @@
}
private void simConfigChanged() {
+ // If mRequestRestrictedWifiEnabled is false, constructor calls simConfigChanged
+ if (mRequestRestrictedWifiEnabled) {
+ ensureRunningOnHandlerThread();
+ }
synchronized (mLock) {
unregisterCarrierPrivilegesListeners();
mModemCount = mTelephonyManager.getActiveModemCount();
@@ -142,6 +178,29 @@
}
}
+ private static class CarrierServiceUidWithSubId {
+ final int mUid;
+ final int mSubId;
+
+ CarrierServiceUidWithSubId(int uid, int subId) {
+ mUid = uid;
+ mSubId = subId;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof CarrierServiceUidWithSubId)) {
+ return false;
+ }
+ CarrierServiceUidWithSubId compare = (CarrierServiceUidWithSubId) obj;
+ return (mUid == compare.mUid && mSubId == compare.mSubId);
+ }
+
+ @Override
+ public int hashCode() {
+ return mUid * 31 + mSubId;
+ }
+ }
private class PrivilegeListener implements CarrierPrivilegesListenerShim {
public final int mLogicalSlot;
@@ -153,6 +212,7 @@
public void onCarrierPrivilegesChanged(
@NonNull List<String> privilegedPackageNames,
@NonNull int[] privilegedUids) {
+ ensureRunningOnHandlerThread();
if (mUseCallbacksForServiceChanged) return;
// Re-trigger the synchronous check (which is also very cheap due
// to caching in CarrierPrivilegesTracker). This allows consistency
@@ -163,6 +223,7 @@
@Override
public void onCarrierServiceChanged(@Nullable final String carrierServicePackageName,
final int carrierServiceUid) {
+ ensureRunningOnHandlerThread();
if (!mUseCallbacksForServiceChanged) {
// Re-trigger the synchronous check (which is also very cheap due
// to caching in CarrierPrivilegesTracker). This allows consistency
@@ -171,7 +232,18 @@
return;
}
synchronized (mLock) {
- mCarrierServiceUid.put(mLogicalSlot, carrierServiceUid);
+ CarrierServiceUidWithSubId oldPair =
+ mCarrierServiceUidWithSubId.get(mLogicalSlot);
+ int subId = getSubId(mLogicalSlot);
+ mCarrierServiceUidWithSubId.put(
+ mLogicalSlot,
+ new CarrierServiceUidWithSubId(carrierServiceUid, subId));
+ if (oldPair != null
+ && oldPair.mUid != Process.INVALID_UID
+ && oldPair.mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ && !oldPair.equals(mCarrierServiceUidWithSubId.get(mLogicalSlot))) {
+ mListener.accept(oldPair.mUid, oldPair.mSubId);
+ }
}
}
}
@@ -193,7 +265,14 @@
private void unregisterCarrierPrivilegesListeners() {
for (PrivilegeListener carrierPrivilegesListener : mCarrierPrivilegesChangedListeners) {
removeCarrierPrivilegesListener(carrierPrivilegesListener);
- mCarrierServiceUid.delete(carrierPrivilegesListener.mLogicalSlot);
+ CarrierServiceUidWithSubId oldPair =
+ mCarrierServiceUidWithSubId.get(carrierPrivilegesListener.mLogicalSlot);
+ mCarrierServiceUidWithSubId.remove(carrierPrivilegesListener.mLogicalSlot);
+ if (oldPair != null
+ && oldPair.mUid != Process.INVALID_UID
+ && oldPair.mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+ mListener.accept(oldPair.mUid, oldPair.mSubId);
+ }
}
mCarrierPrivilegesChangedListeners.clear();
}
@@ -230,8 +309,24 @@
*/
public boolean isCarrierServiceUidForNetworkCapabilities(int callingUid,
@NonNull NetworkCapabilities networkCapabilities) {
- if (callingUid == Process.INVALID_UID) return false;
- final int subId;
+ if (callingUid == Process.INVALID_UID) {
+ return false;
+ }
+ int subId = getSubIdFromNetworkCapabilities(networkCapabilities);
+ if (SubscriptionManager.INVALID_SUBSCRIPTION_ID == subId) {
+ return false;
+ }
+ return callingUid == getCarrierServiceUidForSubId(subId);
+ }
+
+ /**
+ * Extract the SubscriptionId from the NetworkCapabilities.
+ *
+ * @param networkCapabilities the network capabilities which may contains the SubscriptionId.
+ * @return the SubscriptionId.
+ */
+ public int getSubIdFromNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
+ int subId;
if (networkCapabilities.hasSingleTransportBesidesTest(TRANSPORT_CELLULAR)) {
subId = getSubIdFromTelephonySpecifier(networkCapabilities.getNetworkSpecifier());
} else if (networkCapabilities.hasSingleTransportBesidesTest(TRANSPORT_WIFI)) {
@@ -239,6 +334,12 @@
} else {
subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
}
+ if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ && mRequestRestrictedWifiEnabled
+ && networkCapabilities.getSubscriptionIds().size() == 1) {
+ subId = networkCapabilities.getSubscriptionIds().toArray(new Integer[0])[0];
+ }
+
if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
&& !networkCapabilities.getSubscriptionIds().contains(subId)) {
// Ideally, the code above should just use networkCapabilities.getSubscriptionIds()
@@ -250,34 +351,60 @@
Log.wtf(TAG, "NetworkCapabilities subIds are inconsistent between "
+ "specifier/transportInfo and mSubIds : " + networkCapabilities);
}
- if (SubscriptionManager.INVALID_SUBSCRIPTION_ID == subId) return false;
- return callingUid == getCarrierServiceUidForSubId(subId);
+ return subId;
+ }
+
+ @VisibleForTesting
+ protected int getSubId(int slotIndex) {
+ if (SdkLevel.isAtLeastU()) {
+ return SubscriptionManager.getSubscriptionId(slotIndex);
+ } else {
+ SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class);
+ int[] subIds = sm.getSubscriptionIds(slotIndex);
+ if (subIds != null && subIds.length > 0) {
+ return subIds[0];
+ }
+ return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ }
}
@VisibleForTesting
void updateCarrierServiceUid() {
synchronized (mLock) {
- mCarrierServiceUid.clear();
+ SparseArray<CarrierServiceUidWithSubId> copy = mCarrierServiceUidWithSubId.clone();
+ mCarrierServiceUidWithSubId.clear();
for (int i = 0; i < mModemCount; i++) {
- mCarrierServiceUid.put(i, getCarrierServicePackageUidForSlot(i));
+ int subId = getSubId(i);
+ mCarrierServiceUidWithSubId.put(
+ i,
+ new CarrierServiceUidWithSubId(
+ getCarrierServicePackageUidForSlot(i), subId));
+ }
+ for (int i = 0; i < copy.size(); ++i) {
+ CarrierServiceUidWithSubId oldPair = copy.valueAt(i);
+ CarrierServiceUidWithSubId newPair = mCarrierServiceUidWithSubId.get(copy.keyAt(i));
+ if (oldPair.mUid != Process.INVALID_UID
+ && oldPair.mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ && !oldPair.equals(newPair)) {
+ mListener.accept(oldPair.mUid, oldPair.mSubId);
+ }
}
}
}
@VisibleForTesting
int getCarrierServiceUidForSubId(int subId) {
- final int slotId = getSlotIndex(subId);
synchronized (mLock) {
- return mCarrierServiceUid.get(slotId, Process.INVALID_UID);
+ for (int i = 0; i < mCarrierServiceUidWithSubId.size(); ++i) {
+ if (mCarrierServiceUidWithSubId.valueAt(i).mSubId == subId) {
+ return mCarrierServiceUidWithSubId.valueAt(i).mUid;
+ }
+ }
+ return Process.INVALID_UID;
}
}
@VisibleForTesting
- protected int getSlotIndex(int subId) {
- return SubscriptionManager.getSlotIndex(subId);
- }
-
- @VisibleForTesting
int getUidForPackage(String pkgName) {
if (pkgName == null) {
return Process.INVALID_UID;
@@ -338,14 +465,23 @@
}
}
+ private void ensureRunningOnHandlerThread() {
+ if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+ throw new IllegalStateException(
+ "Not running on handler thread: " + Thread.currentThread().getName());
+ }
+ }
+
public void dump(IndentingPrintWriter pw) {
pw.println("CarrierPrivilegeAuthenticator:");
+ pw.println("mRequestRestrictedWifiEnabled = " + mRequestRestrictedWifiEnabled);
synchronized (mLock) {
- final int size = mCarrierServiceUid.size();
- for (int i = 0; i < size; ++i) {
- final int logicalSlot = mCarrierServiceUid.keyAt(i);
- final int serviceUid = mCarrierServiceUid.valueAt(i);
- pw.println("Logical slot = " + logicalSlot + " : uid = " + serviceUid);
+ for (int i = 0; i < mCarrierServiceUidWithSubId.size(); ++i) {
+ final int logicalSlot = mCarrierServiceUidWithSubId.keyAt(i);
+ final int serviceUid = mCarrierServiceUidWithSubId.valueAt(i).mUid;
+ final int subId = mCarrierServiceUidWithSubId.valueAt(i).mSubId;
+ pw.println("Logical slot = " + logicalSlot + " : uid = " + serviceUid
+ + " : subId = " + subId);
}
}
}
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index 17de146..eea16bf 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;
@@ -847,12 +847,12 @@
if (mIngressMap.isEmpty()) {
pw.println("<empty>");
}
- pw.println("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif");
+ pw.println("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif (packets bytes)");
pw.increaseIndent();
mIngressMap.forEach((k, v) -> {
// TODO: print interface name
- pw.println(String.format("%d %s/96 %s -> %s %d", k.iif, k.pfx96, k.local6,
- v.local4, v.oif));
+ pw.println(String.format("%d %s/96 %s -> %s %d (%d %d)", k.iif, k.pfx96, k.local6,
+ v.local4, v.oif, v.packets, v.bytes));
});
pw.decreaseIndent();
} catch (ErrnoException e) {
@@ -870,12 +870,13 @@
if (mEgressMap.isEmpty()) {
pw.println("<empty>");
}
- pw.println("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif");
+ pw.println("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif (packets bytes)");
pw.increaseIndent();
mEgressMap.forEach((k, v) -> {
// TODO: print interface name
- pw.println(String.format("%d %s -> %s %s/96 %d %s", k.iif, k.local4, v.local6,
- v.pfx96, v.oif, v.oifIsEthernet != 0 ? "ether" : "rawip"));
+ pw.println(String.format("%d %s -> %s %s/96 %d %s (%d %d)", k.iif, k.local4,
+ v.local6, v.pfx96, v.oif, v.oifIsEthernet != 0 ? "ether" : "rawip",
+ v.packets, v.bytes));
});
pw.decreaseIndent();
} catch (ErrnoException e) {
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index f8f76ef..176307d 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -36,6 +36,14 @@
public static final String CARRIER_SERVICE_CHANGED_USE_CALLBACK =
"carrier_service_changed_use_callback_version";
+ public static final String REQUEST_RESTRICTED_WIFI =
+ "request_restricted_wifi";
+
+ public static final String INGRESS_TO_VPN_ADDRESS_FILTERING =
+ "ingress_to_vpn_address_filtering";
+
+ public static final String BACKGROUND_FIREWALL_CHAIN = "background_firewall_chain";
+
private boolean mNoRematchAllRequestsOnRegister;
/**
diff --git a/service/src/com/android/server/connectivity/DscpPolicyTracker.java b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
index 8d566b6..15d6adb 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyTracker.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
@@ -85,10 +85,10 @@
public DscpPolicyTracker() throws ErrnoException {
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);
- mBpfDscpIpv6Policies = new BpfMap<Struct.S32, DscpPolicyValue>(IPV6_POLICY_MAP_PATH,
- BpfMap.BPF_F_RDWR, Struct.S32.class, DscpPolicyValue.class);
+ mBpfDscpIpv4Policies = new BpfMap<>(IPV4_POLICY_MAP_PATH,
+ Struct.S32.class, DscpPolicyValue.class);
+ mBpfDscpIpv6Policies = new BpfMap<>(IPV6_POLICY_MAP_PATH,
+ 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/KeepaliveStatsTracker.java b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
index 7a8b41b..21dbb45 100644
--- a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
@@ -29,6 +29,7 @@
import android.net.TelephonyNetworkSpecifier;
import android.net.TransportInfo;
import android.net.wifi.WifiInfo;
+import android.os.Build;
import android.os.Handler;
import android.os.SystemClock;
import android.telephony.SubscriptionInfo;
@@ -39,6 +40,8 @@
import android.util.SparseArray;
import android.util.SparseIntArray;
+import androidx.annotation.RequiresApi;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.metrics.DailykeepaliveInfoReported;
import com.android.metrics.DurationForNumOfKeepalive;
@@ -74,9 +77,10 @@
public class KeepaliveStatsTracker {
private static final String TAG = KeepaliveStatsTracker.class.getSimpleName();
private static final int INVALID_KEEPALIVE_ID = -1;
- // 1 hour acceptable deviation in metrics collection duration time.
+ // 2 hour acceptable deviation in metrics collection duration time to account for the 1 hour
+ // window of AlarmManager.
private static final long MAX_EXPECTED_DURATION_MS =
- AutomaticOnOffKeepaliveTracker.METRICS_COLLECTION_DURATION_MS + 1 * 60 * 60 * 1_000L;
+ AutomaticOnOffKeepaliveTracker.METRICS_COLLECTION_DURATION_MS + 2 * 60 * 60 * 1_000L;
@NonNull private final Handler mConnectivityServiceHandler;
@NonNull private final Dependencies mDependencies;
@@ -278,6 +282,7 @@
*
* @param dailyKeepaliveInfoReported the proto to write to statsD.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void writeStats(DailykeepaliveInfoReported dailyKeepaliveInfoReported) {
ConnectivityStatsLog.write(
ConnectivityStatsLog.DAILY_KEEPALIVE_INFO_REPORTED,
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/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 50cad45..76993a6 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -1551,7 +1551,7 @@
* @param hasAutomotiveFeature true if this device has the automotive feature, false otherwise
* @param authenticator the carrier privilege authenticator to check for telephony constraints
*/
- public void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
+ public static void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
final int creatorUid, final boolean hasAutomotiveFeature,
@NonNull final ConnectivityService.Dependencies deps,
@Nullable final CarrierPrivilegeAuthenticator authenticator) {
@@ -1564,7 +1564,7 @@
}
}
- private boolean areAllowedUidsAcceptableFromNetworkAgent(
+ private static boolean areAllowedUidsAcceptableFromNetworkAgent(
@NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
@NonNull final ConnectivityService.Dependencies deps,
@Nullable final CarrierPrivilegeAuthenticator carrierPrivilegeAuthenticator) {
diff --git a/service/src/com/android/metrics/NetworkRequestStateInfo.java b/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java
similarity index 84%
rename from service/src/com/android/metrics/NetworkRequestStateInfo.java
rename to service/src/com/android/server/connectivity/NetworkRequestStateInfo.java
index e3e172a..ab3d315 100644
--- a/service/src/com/android/metrics/NetworkRequestStateInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.metrics;
+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;
@@ -48,6 +48,15 @@
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) (
diff --git a/service/src/com/android/metrics/NetworkRequestStateStatsMetrics.java b/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java
similarity index 60%
rename from service/src/com/android/metrics/NetworkRequestStateStatsMetrics.java
rename to service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java
index 361ad22..1bc654a 100644
--- a/service/src/com/android/metrics/NetworkRequestStateStatsMetrics.java
+++ b/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.metrics;
+package com.android.server.connectivity;
import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED;
@@ -31,6 +31,8 @@
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.
@@ -44,17 +46,21 @@
public class NetworkRequestStateStatsMetrics {
private static final String TAG = "NetworkRequestStateStatsMetrics";
- private static final int MSG_NETWORK_REQUEST_STATE_CHANGED = 0;
+ 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;
- // 1 second internal is suggested by experiment team
- private static final int ATOM_INTERVAL_MS = 1000;
- private final SparseArray<NetworkRequestStateInfo> mNetworkRequestsActive;
+ @VisibleForTesting
+ static final int MAX_QUEUED_REQUESTS = 20;
- private final Handler mStatsLoggingHandler;
+ // 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());
@@ -86,11 +92,10 @@
NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
networkRequest, mNRStateInfoDeps);
mNetworkRequestsActive.put(networkRequest.requestId, networkRequestStateInfo);
- mStatsLoggingHandler.sendMessage(
- Message.obtain(
- mStatsLoggingHandler,
- MSG_NETWORK_REQUEST_STATE_CHANGED,
- networkRequestStateInfo));
+ mStatsLoggingHandler.sendMessage(Message.obtain(
+ mStatsLoggingHandler,
+ CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC,
+ networkRequestStateInfo));
}
}
@@ -106,15 +111,13 @@
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,
- MSG_NETWORK_REQUEST_STATE_CHANGED,
- networkRequestStateInfo));
-
+ mStatsLoggingHandler.sendMessage(Message.obtain(
+ mStatsLoggingHandler,
+ CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC,
+ networkRequestStateInfo));
}
}
@@ -130,16 +133,19 @@
}
/**
- * Sleeps the thread for provided intervalMs millis.
- *
- * @param intervalMs number of millis for the thread sleep.
+ * @see Handler#sendMessageDelayed(Message, long)
*/
- public void threadSleep(int intervalMs) {
- try {
- Thread.sleep(intervalMs);
- } catch (InterruptedException e) {
- Log.w(TAG, "Cool down interrupted!", e);
- }
+ 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;
}
/**
@@ -161,29 +167,59 @@
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 checkStatsLoggingTimeout() {
- // Cool down before next execution. Required by atom logging frequency.
- long now = SystemClock.elapsedRealtime();
- if (now - mLastLogTime < ATOM_INTERVAL_MS) {
- mDependencies.threadSleep(ATOM_INTERVAL_MS);
+ 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;
}
- mLastLogTime = now;
+ 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 MSG_NETWORK_REQUEST_STATE_CHANGED:
- checkStatsLoggingTimeout();
- loggingInfo = (NetworkRequestStateInfo) msg.obj;
- mDependencies.writeStats(loggingInfo);
+ 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/SatelliteAccessController.java b/service/src/com/android/server/connectivity/SatelliteAccessController.java
new file mode 100644
index 0000000..2cdc932
--- /dev/null
+++ b/service/src/com/android/server/connectivity/SatelliteAccessController.java
@@ -0,0 +1,259 @@
+/*
+ * 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 android.Manifest;
+import android.annotation.NonNull;
+import android.app.role.OnRoleHoldersChangedListener;
+import android.app.role.RoleManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Tracks the uid of all the default messaging application which are role_sms role and
+ * satellite_communication permission complaint and requests ConnectivityService to create multi
+ * layer request with satellite internet access support for the default message application.
+ * @hide
+ */
+public class SatelliteAccessController {
+ private static final String TAG = SatelliteAccessController.class.getSimpleName();
+ private final Context mContext;
+ private final Dependencies mDeps;
+ private final DefaultMessageRoleListener mDefaultMessageRoleListener;
+ private final Consumer<Set<Integer>> mCallback;
+ private final Handler mConnectivityServiceHandler;
+
+ // At this sparseArray, Key is userId and values are uids of SMS apps that are allowed
+ // to use satellite network as fallback.
+ private final SparseArray<Set<Integer>> mAllUsersSatelliteNetworkFallbackUidCache =
+ new SparseArray<>();
+
+ /**
+ * Monitor {@link android.app.role.OnRoleHoldersChangedListener#onRoleHoldersChanged(String,
+ * UserHandle)},
+ *
+ */
+ private final class DefaultMessageRoleListener
+ implements OnRoleHoldersChangedListener {
+ @Override
+ public void onRoleHoldersChanged(String role, UserHandle userHandle) {
+ if (RoleManager.ROLE_SMS.equals(role)) {
+ Log.i(TAG, "ROLE_SMS Change detected ");
+ onRoleSmsChanged(userHandle);
+ }
+ }
+
+ public void register() {
+ try {
+ mDeps.addOnRoleHoldersChangedListenerAsUser(
+ mConnectivityServiceHandler::post, this, UserHandle.ALL);
+ } catch (RuntimeException e) {
+ Log.wtf(TAG, "Could not register satellite controller listener due to " + e);
+ }
+ }
+ }
+
+ public SatelliteAccessController(@NonNull final Context c,
+ Consumer<Set<Integer>> callback,
+ @NonNull final Handler connectivityServiceInternalHandler) {
+ this(c, new Dependencies(c), callback, connectivityServiceInternalHandler);
+ }
+
+ public static class Dependencies {
+ private final RoleManager mRoleManager;
+
+ private Dependencies(Context context) {
+ mRoleManager = context.getSystemService(RoleManager.class);
+ }
+
+ /** See {@link RoleManager#getRoleHoldersAsUser(String, UserHandle)} */
+ public List<String> getRoleHoldersAsUser(String roleName, UserHandle userHandle) {
+ return mRoleManager.getRoleHoldersAsUser(roleName, userHandle);
+ }
+
+ /** See {@link RoleManager#addOnRoleHoldersChangedListenerAsUser} */
+ public void addOnRoleHoldersChangedListenerAsUser(@NonNull Executor executor,
+ @NonNull OnRoleHoldersChangedListener listener, UserHandle user) {
+ mRoleManager.addOnRoleHoldersChangedListenerAsUser(executor, listener, user);
+ }
+ }
+
+ @VisibleForTesting
+ SatelliteAccessController(@NonNull final Context c, @NonNull final Dependencies deps,
+ Consumer<Set<Integer>> callback,
+ @NonNull final Handler connectivityServiceInternalHandler) {
+ mContext = c;
+ mDeps = deps;
+ mDefaultMessageRoleListener = new DefaultMessageRoleListener();
+ mCallback = callback;
+ mConnectivityServiceHandler = connectivityServiceInternalHandler;
+ }
+
+ private Set<Integer> updateSatelliteNetworkFallbackUidListCache(List<String> packageNames,
+ @NonNull UserHandle userHandle) {
+ Set<Integer> fallbackUids = new ArraySet<>();
+ PackageManager pm =
+ mContext.createContextAsUser(userHandle, 0).getPackageManager();
+ if (pm != null) {
+ for (String packageName : packageNames) {
+ // Check if SATELLITE_COMMUNICATION permission is enabled for default sms
+ // application package before adding it part of satellite network fallback uid
+ // cache list.
+ if (isSatellitePermissionEnabled(pm, packageName)) {
+ int uid = getUidForPackage(pm, packageName);
+ if (uid != Process.INVALID_UID) {
+ fallbackUids.add(uid);
+ }
+ }
+ }
+ } else {
+ Log.wtf(TAG, "package manager found null");
+ }
+ return fallbackUids;
+ }
+
+ //Check if satellite communication is enabled for the package
+ private boolean isSatellitePermissionEnabled(PackageManager packageManager,
+ String packageName) {
+ return packageManager.checkPermission(
+ Manifest.permission.SATELLITE_COMMUNICATION, packageName)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private int getUidForPackage(PackageManager packageManager, String pkgName) {
+ if (pkgName == null) {
+ return Process.INVALID_UID;
+ }
+ try {
+ ApplicationInfo applicationInfo = packageManager.getApplicationInfo(pkgName, 0);
+ return applicationInfo.uid;
+ } catch (PackageManager.NameNotFoundException exception) {
+ Log.e(TAG, "Unable to find uid for package: " + pkgName);
+ }
+ return Process.INVALID_UID;
+ }
+
+ // on Role sms change triggered by OnRoleHoldersChangedListener()
+ private void onRoleSmsChanged(@NonNull UserHandle userHandle) {
+ int userId = userHandle.getIdentifier();
+ if (userId == Process.INVALID_UID) {
+ Log.wtf(TAG, "Invalid User Id");
+ return;
+ }
+
+ //Returns empty list if no package exists
+ final List<String> packageNames =
+ mDeps.getRoleHoldersAsUser(RoleManager.ROLE_SMS, userHandle);
+
+ // Store previous satellite fallback uid available
+ final Set<Integer> prevUidsForUser =
+ mAllUsersSatelliteNetworkFallbackUidCache.get(userId, new ArraySet<>());
+
+ Log.i(TAG, "currentUser : role_sms_packages: " + userId + " : " + packageNames);
+ final Set<Integer> newUidsForUser =
+ updateSatelliteNetworkFallbackUidListCache(packageNames, userHandle);
+ Log.i(TAG, "satellite_fallback_uid: " + newUidsForUser);
+
+ // on Role change, update the multilayer request at ConnectivityService with updated
+ // satellite network fallback uid cache list of multiple users as applicable
+ if (newUidsForUser.equals(prevUidsForUser)) {
+ return;
+ }
+
+ mAllUsersSatelliteNetworkFallbackUidCache.put(userId, newUidsForUser);
+
+ // Update all users fallback cache for user, send cs fallback to update ML request
+ reportSatelliteNetworkFallbackUids();
+ }
+
+ private void reportSatelliteNetworkFallbackUids() {
+ // Merge all uids of multiple users available
+ Set<Integer> mergedSatelliteNetworkFallbackUidCache = new ArraySet<>();
+ for (int i = 0; i < mAllUsersSatelliteNetworkFallbackUidCache.size(); i++) {
+ mergedSatelliteNetworkFallbackUidCache.addAll(
+ mAllUsersSatelliteNetworkFallbackUidCache.valueAt(i));
+ }
+ Log.i(TAG, "merged uid list for multi layer request : "
+ + mergedSatelliteNetworkFallbackUidCache);
+
+ // trigger multiple layer request for satellite network fallback of multi user uids
+ mCallback.accept(mergedSatelliteNetworkFallbackUidCache);
+ }
+
+ public void start() {
+ mConnectivityServiceHandler.post(this::updateAllUserRoleSmsUids);
+
+ // register sms OnRoleHoldersChangedListener
+ mDefaultMessageRoleListener.register();
+
+ // Monitor for User removal intent, to update satellite fallback uids.
+ IntentFilter userRemovedFilter = new IntentFilter(Intent.ACTION_USER_REMOVED);
+ mContext.registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (Intent.ACTION_USER_REMOVED.equals(action)) {
+ final UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER);
+ if (userHandle == null) return;
+ updateSatelliteFallbackUidListOnUserRemoval(userHandle.getIdentifier());
+ } else {
+ Log.wtf(TAG, "received unexpected intent: " + action);
+ }
+ }
+ }, userRemovedFilter, null, mConnectivityServiceHandler);
+
+ }
+
+ private void updateAllUserRoleSmsUids() {
+ UserManager userManager = mContext.getSystemService(UserManager.class);
+ // get existing user handles of available users
+ List<UserHandle> existingUsers = userManager.getUserHandles(true /*excludeDying*/);
+
+ // Iterate through the user handles and obtain their uids with role sms and satellite
+ // communication permission
+ Log.i(TAG, "existing users: " + existingUsers);
+ for (UserHandle userHandle : existingUsers) {
+ onRoleSmsChanged(userHandle);
+ }
+ }
+
+ private void updateSatelliteFallbackUidListOnUserRemoval(int userIdRemoved) {
+ Log.i(TAG, "user id removed:" + userIdRemoved);
+ if (mAllUsersSatelliteNetworkFallbackUidCache.contains(userIdRemoved)) {
+ mAllUsersSatelliteNetworkFallbackUidCache.remove(userIdRemoved);
+ reportSatelliteNetworkFallbackUids();
+ }
+ }
+}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 6f7ea4c..ede6d3f 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -28,33 +28,35 @@
// though they are not in the current.txt files.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
java_library {
- name: "net-utils-device-common",
- srcs: [
- "device/com/android/net/module/util/arp/ArpPacket.java",
- "device/com/android/net/module/util/DeviceConfigUtils.java",
- "device/com/android/net/module/util/DomainUtils.java",
- "device/com/android/net/module/util/FdEventsReader.java",
- "device/com/android/net/module/util/NetworkMonitorUtils.java",
- "device/com/android/net/module/util/PacketReader.java",
- "device/com/android/net/module/util/SharedLog.java",
- "device/com/android/net/module/util/SocketUtils.java",
- "device/com/android/net/module/util/FeatureVersions.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",
- ],
- sdk_version: "module_current",
- min_sdk_version: "30",
- target_sdk_version: "30",
- apex_available: [
- "//apex_available:anyapex",
- "//apex_available:platform",
- ],
- visibility: [
+ name: "net-utils-device-common",
+ srcs: [
+ "device/com/android/net/module/util/arp/ArpPacket.java",
+ "device/com/android/net/module/util/DeviceConfigUtils.java",
+ "device/com/android/net/module/util/DomainUtils.java",
+ "device/com/android/net/module/util/FdEventsReader.java",
+ "device/com/android/net/module/util/NetworkMonitorUtils.java",
+ "device/com/android/net/module/util/PacketReader.java",
+ "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",
+ ],
+ sdk_version: "module_current",
+ min_sdk_version: "30",
+ target_sdk_version: "30",
+ apex_available: [
+ "//apex_available:anyapex",
+ "//apex_available:platform",
+ ],
+ visibility: [
"//frameworks/base/packages/Tethering",
"//packages/modules/Connectivity:__subpackages__",
"//packages/modules/Connectivity/framework:__subpackages__",
@@ -64,26 +66,26 @@
"//frameworks/opt/net/telephony",
"//packages/modules/NetworkStack:__subpackages__",
"//packages/modules/CaptivePortalLogin",
- ],
- static_libs: [
- "net-utils-framework-common",
- ],
- libs: [
- "androidx.annotation_annotation",
- "framework-annotations-lib",
- "framework-configinfrastructure",
- "framework-connectivity.stubs.module_lib",
- ],
- lint: {
- strict_updatability_linting: true,
- error_checks: ["NewApi"],
- },
+ ],
+ static_libs: [
+ "net-utils-framework-common",
+ ],
+ libs: [
+ "androidx.annotation_annotation",
+ "framework-annotations-lib",
+ "framework-configinfrastructure",
+ "framework-connectivity.stubs.module_lib",
+ ],
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
}
java_defaults {
name: "lib_mockito_extended",
static_libs: [
- "mockito-target-extended-minus-junit4"
+ "mockito-target-extended-minus-junit4",
],
jni_libs: [
"libdexmakerjvmtiagent",
@@ -94,12 +96,12 @@
java_library {
name: "net-utils-dnspacket-common",
srcs: [
- "framework/**/DnsPacket.java",
- "framework/**/DnsPacketUtils.java",
- "framework/**/DnsSvcbPacket.java",
- "framework/**/DnsSvcbRecord.java",
- "framework/**/HexDump.java",
- "framework/**/NetworkStackConstants.java",
+ "framework/**/DnsPacket.java",
+ "framework/**/DnsPacketUtils.java",
+ "framework/**/DnsSvcbPacket.java",
+ "framework/**/DnsSvcbRecord.java",
+ "framework/**/HexDump.java",
+ "framework/**/NetworkStackConstants.java",
],
sdk_version: "module_current",
visibility: [
@@ -122,6 +124,8 @@
],
}
+// The net-utils-device-common-bpf library requires the callers to contain
+// net-utils-device-common-struct-base.
java_library {
name: "net-utils-device-common-bpf",
srcs: [
@@ -131,9 +135,7 @@
"device/com/android/net/module/util/BpfUtils.java",
"device/com/android/net/module/util/IBpfMap.java",
"device/com/android/net/module/util/JniUtil.java",
- "device/com/android/net/module/util/Struct.java",
"device/com/android/net/module/util/TcUtils.java",
- "framework/com/android/net/module/util/HexDump.java",
],
sdk_version: "module_current",
min_sdk_version: "30",
@@ -144,6 +146,7 @@
libs: [
"androidx.annotation_annotation",
"framework-connectivity.stubs.module_lib",
+ "net-utils-device-common-struct-base",
],
apex_available: [
"com.android.tethering",
@@ -156,12 +159,9 @@
}
java_library {
- name: "net-utils-device-common-struct",
+ name: "net-utils-device-common-struct-base",
srcs: [
- "device/com/android/net/module/util/Ipv6Utils.java",
- "device/com/android/net/module/util/PacketBuilder.java",
"device/com/android/net/module/util/Struct.java",
- "device/com/android/net/module/util/structs/*.java",
],
sdk_version: "module_current",
min_sdk_version: "30",
@@ -174,6 +174,7 @@
],
libs: [
"androidx.annotation_annotation",
+ "framework-annotations-lib", // Required by InetAddressUtils.java
"framework-connectivity.stubs.module_lib",
],
apex_available: [
@@ -186,8 +187,39 @@
},
}
+// The net-utils-device-common-struct library requires the callers to contain
+// net-utils-device-common-struct-base.
+java_library {
+ name: "net-utils-device-common-struct",
+ srcs: [
+ "device/com/android/net/module/util/Ipv6Utils.java",
+ "device/com/android/net/module/util/PacketBuilder.java",
+ "device/com/android/net/module/util/structs/*.java",
+ ],
+ sdk_version: "module_current",
+ min_sdk_version: "30",
+ visibility: [
+ "//packages/modules/Connectivity:__subpackages__",
+ "//packages/modules/NetworkStack:__subpackages__",
+ ],
+ libs: [
+ "androidx.annotation_annotation",
+ "framework-annotations-lib", // Required by IpUtils.java
+ "framework-connectivity.stubs.module_lib",
+ "net-utils-device-common-struct-base",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ "//apex_available:platform",
+ ],
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
+}
+
// The net-utils-device-common-netlink library requires the callers to contain
-// net-utils-device-common-struct.
+// net-utils-device-common-struct and net-utils-device-common-struct-base.
java_library {
name: "net-utils-device-common-netlink",
srcs: [
@@ -206,6 +238,7 @@
// statically link here because callers of this library might already have a static
// version linked.
"net-utils-device-common-struct",
+ "net-utils-device-common-struct-base",
],
apex_available: [
"com.android.tethering",
@@ -218,7 +251,7 @@
}
// The net-utils-device-common-ip library requires the callers to contain
-// net-utils-device-common-struct.
+// net-utils-device-common-struct and net-utils-device-common-struct-base.
java_library {
// TODO : this target should probably be folded into net-utils-device-common
name: "net-utils-device-common-ip",
@@ -246,7 +279,7 @@
"//apex_available:platform",
],
lint: {
- strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
error_checks: ["NewApi"],
},
}
@@ -270,13 +303,11 @@
"//cts/tests/tests/net",
"//cts/tests/tests/wifi",
"//packages/modules/Connectivity/tests/cts/net",
- "//frameworks/base/packages/Tethering",
"//packages/modules/Connectivity/Tethering",
"//frameworks/base/tests:__subpackages__",
"//frameworks/opt/net/ike",
"//frameworks/opt/telephony",
"//frameworks/base/wifi:__subpackages__",
- "//frameworks/base/packages/Connectivity:__subpackages__",
"//packages/modules/Connectivity:__subpackages__",
"//packages/modules/NetworkStack:__subpackages__",
"//packages/modules/CaptivePortalLogin",
@@ -463,10 +494,10 @@
filegroup {
name: "net-utils-wifi-service-common-srcs",
srcs: [
- "device/android/net/NetworkFactory.java",
- "device/android/net/NetworkFactoryImpl.java",
- "device/android/net/NetworkFactoryLegacyImpl.java",
- "device/android/net/NetworkFactoryShim.java",
+ "device/android/net/NetworkFactory.java",
+ "device/android/net/NetworkFactoryImpl.java",
+ "device/android/net/NetworkFactoryLegacyImpl.java",
+ "device/android/net/NetworkFactoryShim.java",
],
visibility: [
"//frameworks/opt/net/wifi/service",
diff --git a/staticlibs/client-libs/Android.bp b/staticlibs/client-libs/Android.bp
index c938dd6..f665584 100644
--- a/staticlibs/client-libs/Android.bp
+++ b/staticlibs/client-libs/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -10,17 +11,17 @@
apex_available: [
"//apex_available:platform",
"com.android.tethering",
- "com.android.wifi"
+ "com.android.wifi",
],
visibility: [
"//packages/modules/Connectivity:__subpackages__",
"//frameworks/base/services:__subpackages__",
"//frameworks/base/packages:__subpackages__",
- "//packages/modules/Wifi/service:__subpackages__"
+ "//packages/modules/Wifi/service:__subpackages__",
],
libs: ["androidx.annotation_annotation"],
static_libs: [
"netd_aidl_interface-lateststable-java",
- "netd_event_listener_interface-lateststable-java"
- ]
+ "netd_event_listener_interface-lateststable-java",
+ ],
}
diff --git a/staticlibs/client-libs/tests/unit/Android.bp b/staticlibs/client-libs/tests/unit/Android.bp
index 03e3e70..7aafd69 100644
--- a/staticlibs/client-libs/tests/unit/Android.bp
+++ b/staticlibs/client-libs/tests/unit/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -26,7 +27,7 @@
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/NetworkStack/tests/integration",
- ]
+ ],
}
android_test {
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/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
index 42f26f4..0426ace 100644
--- a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -17,6 +17,7 @@
package com.android.net.module.util;
import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+import static android.provider.DeviceConfig.NAMESPACE_CAPTIVEPORTALLOGIN;
import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
@@ -64,9 +65,6 @@
@VisibleForTesting
public static final long DEFAULT_PACKAGE_VERSION = 1000;
- private static final String CORE_NETWORKING_TRUNK_STABLE_NAMESPACE = "android_core_networking";
- private static final String CORE_NETWORKING_TRUNK_STABLE_FLAG_PACKAGE = "com.android.net.flags";
-
@VisibleForTesting
public static void resetPackageVersionCacheForTest() {
sPackageVersion = -1;
@@ -206,6 +204,29 @@
() -> getTetheringModuleVersion(context));
}
+ /**
+ * Check whether or not one specific experimental feature for a particular namespace from
+ * {@link DeviceConfig} is enabled by comparing module package version
+ * with current version of property. If this property version is valid, the corresponding
+ * experimental feature would be enabled, otherwise disabled.
+ *
+ * This is useful to ensure that if a module install is rolled back, flags are not left fully
+ * rolled out on a version where they have not been well tested.
+ *
+ * If the feature is disabled by default and enabled by flag push, this method should be used.
+ * If the feature is enabled by default and disabled by flag push (kill switch),
+ * {@link #isCaptivePortalLoginFeatureNotChickenedOut(Context, String)} should be used.
+ *
+ * @param context The global context information about an app environment.
+ * @param name The name of the property to look up.
+ * @return true if this feature is enabled, or false if disabled.
+ */
+ public static boolean isCaptivePortalLoginFeatureEnabled(@NonNull Context context,
+ @NonNull String name) {
+ return isFeatureEnabled(NAMESPACE_CAPTIVEPORTALLOGIN, name, false /* defaultEnabled */,
+ () -> getPackageVersion(context));
+ }
+
private static boolean isFeatureEnabled(@NonNull String namespace,
String name, boolean defaultEnabled, Supplier<Long> packageVersionSupplier) {
final int flagValue = getDeviceConfigPropertyInt(namespace, name, 0 /* default value */);
@@ -409,31 +430,4 @@
return pkgs.get(0).activityInfo.applicationInfo.packageName;
}
-
- /**
- * Check whether one specific trunk stable flag in android_core_networking namespace is enabled.
- * This method reads trunk stable feature flag value from DeviceConfig directly since
- * java_aconfig_library soong module is not available in the mainline branch.
- * After the mainline branch support the aconfig soong module, this function must be removed and
- * java_aconfig_library must be used instead to check if the feature is enabled.
- *
- * @param flagName The name of the trunk stable flag
- * @return true if this feature is enabled, or false if disabled.
- */
- public static boolean isTrunkStableFeatureEnabled(final String flagName) {
- return isTrunkStableFeatureEnabled(
- CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
- CORE_NETWORKING_TRUNK_STABLE_FLAG_PACKAGE,
- flagName
- );
- }
-
- private static boolean isTrunkStableFeatureEnabled(final String namespace,
- final String packageName, final String flagName) {
- return DeviceConfig.getBoolean(
- namespace,
- packageName + "." + flagName,
- false /* defaultValue */
- );
- }
}
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/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 dbd83d0..fecaa09 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
@@ -24,15 +24,14 @@
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.SOCKDIAG_MSG_HEADER_SIZE;
-import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
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;
import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
+import static com.android.net.module.util.netlink.NetlinkUtils.SOCKET_RECV_BUFSIZE;
import static com.android.net.module.util.netlink.NetlinkUtils.TCP_ALIVE_STATE_FILTER;
import static com.android.net.module.util.netlink.NetlinkUtils.connectToKernel;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
@@ -182,7 +181,10 @@
while (payload.position() < payloadLength) {
final StructNlAttr attr = StructNlAttr.parse(payload);
// Stop parsing for truncated or malformed attribute
- if (attr == null) return null;
+ if (attr == null) {
+ Log.wtf(TAG, "Got truncated or malformed attribute");
+ return null;
+ }
msg.nlAttrs.add(attr);
}
@@ -280,7 +282,7 @@
int uid = INVALID_UID;
FileDescriptor fd = null;
try {
- fd = NetlinkUtils.netlinkSocketForProto(NETLINK_INET_DIAG);
+ fd = NetlinkUtils.netlinkSocketForProto(NETLINK_INET_DIAG, SOCKET_RECV_BUFSIZE);
connectToKernel(fd);
uid = lookupUid(protocol, local, remote, fd);
} catch (ErrnoException | SocketException | IllegalArgumentException
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NdOption.java b/staticlibs/device/com/android/net/module/util/netlink/NdOption.java
index defc88a..4f58380 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NdOption.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NdOption.java
@@ -67,6 +67,9 @@
case StructNdOptRdnss.TYPE:
return StructNdOptRdnss.parse(buf);
+ case StructNdOptPio.TYPE:
+ return StructNdOptPio.parse(buf);
+
default:
int newPosition = Math.min(buf.limit(), buf.position() + length * 8);
buf.position(newPosition);
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
index bdf574d..586f19d 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
@@ -20,6 +20,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import java.net.Inet6Address;
import java.net.InetAddress;
@@ -63,6 +64,20 @@
/** The IP address that sent the packet containing the option. */
public final InetAddress srcaddr;
+ @VisibleForTesting
+ public NduseroptMessage(@NonNull final StructNlMsgHdr header, byte family, int optslen,
+ int ifindex, byte icmptype, byte icmpcode, @NonNull final NdOption option,
+ final InetAddress srcaddr) {
+ super(header);
+ this.family = family;
+ this.opts_len = optslen;
+ this.ifindex = ifindex;
+ this.icmp_type = icmptype;
+ this.icmp_code = icmpcode;
+ this.option = option;
+ this.srcaddr = srcaddr;
+ }
+
NduseroptMessage(@NonNull StructNlMsgHdr header, @NonNull ByteBuffer buf)
throws UnknownHostException {
super(header);
@@ -141,8 +156,9 @@
@Override
public String toString() {
- return String.format("Nduseroptmsg(%d, %d, %d, %d, %d, %s)",
+ return String.format("Nduseroptmsg(family:%d, opts_len:%d, ifindex:%d, icmp_type:%d, "
+ + "icmp_code:%d, srcaddr: %s, %s)",
family, opts_len, ifindex, Byte.toUnsignedInt(icmp_type),
- Byte.toUnsignedInt(icmp_code), srcaddr.getHostAddress());
+ Byte.toUnsignedInt(icmp_code), srcaddr.getHostAddress(), option);
}
}
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 7c2be2c..541a375 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -29,6 +29,7 @@
import static android.system.OsConstants.SO_RCVBUF;
import static android.system.OsConstants.SO_RCVTIMEO;
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;
@@ -49,6 +50,7 @@
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.Inet6Address;
+import java.net.InetAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -56,7 +58,6 @@
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
-import java.util.stream.Collectors;
/**
* Utilities for netlink related class that may not be able to fit into a specific class.
@@ -85,6 +86,7 @@
public static final int DEFAULT_RECV_BUFSIZE = 8 * 1024;
public static final int SOCKET_RECV_BUFSIZE = 64 * 1024;
+ public static final int SOCKET_DUMP_RECV_BUFSIZE = 128 * 1024;
/**
* Return whether the input ByteBuffer contains enough remaining bytes for
@@ -159,7 +161,7 @@
*/
public static void sendOneShotKernelMessage(int nlProto, byte[] msg) throws ErrnoException {
final String errPrefix = "Error in NetlinkSocket.sendOneShotKernelMessage";
- final FileDescriptor fd = netlinkSocketForProto(nlProto);
+ final FileDescriptor fd = netlinkSocketForProto(nlProto, SOCKET_RECV_BUFSIZE);
try {
connectToKernel(fd);
@@ -177,19 +179,19 @@
}
/**
- * Send an RTM_NEWADDR message to kernel to add or update an IPv6 address.
+ * Send an RTM_NEWADDR message to kernel to add or update an IP address.
*
* @param ifIndex interface index.
- * @param ip IPv6 address to be added.
- * @param prefixlen IPv6 address prefix length.
- * @param flags IPv6 address flags.
- * @param scope IPv6 address scope.
- * @param preferred The preferred lifetime of IPv6 address.
- * @param valid The valid lifetime of IPv6 address.
+ * @param ip IP address to be added.
+ * @param prefixlen IP address prefix length.
+ * @param flags IP address flags.
+ * @param scope IP address scope.
+ * @param preferred The preferred lifetime of IP address.
+ * @param valid The valid lifetime of IP address.
*/
- public static boolean sendRtmNewAddressRequest(int ifIndex, @NonNull final Inet6Address ip,
+ public static boolean sendRtmNewAddressRequest(int ifIndex, @NonNull final InetAddress ip,
short prefixlen, int flags, byte scope, long preferred, long valid) {
- Objects.requireNonNull(ip, "IPv6 address to be added should not be null.");
+ Objects.requireNonNull(ip, "IP address to be added should not be null.");
final byte[] msg = RtNetlinkAddressMessage.newRtmNewAddressMessage(1 /* seqNo*/, ip,
prefixlen, flags, scope, ifIndex, preferred, valid);
try {
@@ -223,22 +225,41 @@
}
/**
- * Create netlink socket with the given netlink protocol type.
+ * Create netlink socket with the given netlink protocol type and buffersize.
+ *
+ * @param nlProto the netlink protocol
+ * @param bufferSize the receive buffer size to set when the value is not 0
*
* @return fd the fileDescriptor of the socket.
* @throws ErrnoException if the FileDescriptor not connect to be created successfully
*/
- public static FileDescriptor netlinkSocketForProto(int nlProto) throws ErrnoException {
- final FileDescriptor fd = Os.socket(AF_NETLINK, SOCK_DGRAM, nlProto);
- Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, SOCKET_RECV_BUFSIZE);
+ public static FileDescriptor netlinkSocketForProto(int nlProto, int bufferSize)
+ throws ErrnoException {
+ final FileDescriptor fd = Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, nlProto);
+ if (bufferSize > 0) {
+ Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, bufferSize);
+ }
return fd;
}
/**
+ * Create netlink socket with the given netlink protocol type. Receive buffer size is not set.
+ *
+ * @param nlProto the netlink protocol
+ *
+ * @return fd the fileDescriptor of the socket.
+ * @throws ErrnoException if the FileDescriptor not connect to be created successfully
+ */
+ public static FileDescriptor netlinkSocketForProto(int nlProto)
+ throws ErrnoException {
+ return netlinkSocketForProto(nlProto, 0);
+ }
+
+ /**
* Construct a netlink inet_diag socket.
*/
public static FileDescriptor createNetLinkInetDiagSocket() throws ErrnoException {
- return Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_INET_DIAG);
+ return netlinkSocketForProto(NETLINK_INET_DIAG);
}
/**
@@ -373,7 +394,7 @@
Consumer<T> func)
throws SocketException, InterruptedIOException, ErrnoException {
// Create socket
- final FileDescriptor fd = netlinkSocketForProto(nlFamily);
+ final FileDescriptor fd = netlinkSocketForProto(nlFamily, SOCKET_DUMP_RECV_BUFSIZE);
try {
getAndProcessNetlinkDumpMessagesWithFd(fd, dumpRequestMessage, nlFamily,
msgClass, func);
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
index cbe0ab0..4846df7 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
@@ -16,6 +16,7 @@
package com.android.net.module.util.netlink;
+import static com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_ACK;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REPLACE;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
@@ -28,6 +29,7 @@
import com.android.net.module.util.HexDump;
+import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.nio.ByteBuffer;
@@ -48,6 +50,8 @@
*/
public class RtNetlinkAddressMessage extends NetlinkMessage {
public static final short IFA_ADDRESS = 1;
+ public static final short IFA_LOCAL = 2;
+ public static final short IFA_BROADCAST = 4;
public static final short IFA_CACHEINFO = 6;
public static final short IFA_FLAGS = 8;
@@ -71,6 +75,7 @@
mIfacacheInfo = structIfacacheInfo;
mFlags = flags;
}
+
private RtNetlinkAddressMessage(@NonNull StructNlMsgHdr header) {
this(header, null, null, null, 0);
}
@@ -158,6 +163,24 @@
// still be packed to ByteBuffer even if the flag is 0.
final StructNlAttr flags = new StructNlAttr(IFA_FLAGS, mFlags);
flags.pack(byteBuffer);
+
+ // Add the required IFA_LOCAL and IFA_BROADCAST attributes for IPv4 addresses. The IFA_LOCAL
+ // attribute represents the local address, which is equivalent to IFA_ADDRESS on a normally
+ // configured broadcast interface, however, for PPP interfaces, IFA_ADDRESS indicates the
+ // destination address and the local address is provided in the IFA_LOCAL attribute. If the
+ // IFA_LOCAL attribute is not present in the RTM_NEWADDR message, the kernel replies with an
+ // error netlink message with invalid parameters. IFA_BROADCAST is also required, otherwise
+ // the broadcast on the interface is 0.0.0.0. See include/uapi/linux/if_addr.h for details.
+ // For IPv6 addresses, the IFA_ADDRESS attribute applies and introduces no ambiguity.
+ if (mIpAddress instanceof Inet4Address) {
+ final StructNlAttr localAddress = new StructNlAttr(IFA_LOCAL, mIpAddress);
+ localAddress.pack(byteBuffer);
+
+ final Inet4Address broadcast =
+ getBroadcastAddress((Inet4Address) mIpAddress, mIfaddrmsg.prefixLen);
+ final StructNlAttr broadcastAddress = new StructNlAttr(IFA_BROADCAST, broadcast);
+ broadcastAddress.pack(byteBuffer);
+ }
}
/**
@@ -184,7 +207,7 @@
0 /* tstamp */);
msg.mFlags = flags;
- final byte[] bytes = new byte[msg.getRequiredSpace()];
+ final byte[] bytes = new byte[msg.getRequiredSpace(family)];
nlmsghdr.nlmsg_len = bytes.length;
final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
byteBuffer.order(ByteOrder.nativeOrder());
@@ -237,7 +260,7 @@
// RtNetlinkAddressMessage, e.g. RTM_DELADDR sent from user space to kernel to delete an
// IP address only requires IFA_ADDRESS attribute. The caller should check if these attributes
// are necessary to carry when constructing a RtNetlinkAddressMessage.
- private int getRequiredSpace() {
+ private int getRequiredSpace(int family) {
int spaceRequired = StructNlMsgHdr.STRUCT_SIZE + StructIfaddrMsg.STRUCT_SIZE;
// IFA_ADDRESS attr
spaceRequired += NetlinkConstants.alignedLengthOf(
@@ -247,6 +270,14 @@
StructNlAttr.NLA_HEADERLEN + StructIfacacheInfo.STRUCT_SIZE);
// IFA_FLAGS "u32" attr
spaceRequired += StructNlAttr.NLA_HEADERLEN + 4;
+ if (family == OsConstants.AF_INET) {
+ // IFA_LOCAL attr
+ spaceRequired += NetlinkConstants.alignedLengthOf(
+ StructNlAttr.NLA_HEADERLEN + mIpAddress.getAddress().length);
+ // IFA_BROADCAST attr
+ spaceRequired += NetlinkConstants.alignedLengthOf(
+ StructNlAttr.NLA_HEADERLEN + mIpAddress.getAddress().length);
+ }
return spaceRequired;
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
index b2b1e93..545afeaf 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
@@ -19,10 +19,8 @@
import static android.system.OsConstants.AF_INET;
import static android.system.OsConstants.AF_INET6;
-import static android.system.OsConstants.NETLINK_ROUTE;
import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY;
-import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
import android.annotation.SuppressLint;
@@ -38,9 +36,6 @@
import java.net.Inet6Address;
import java.net.InetAddress;
import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.IntBuffer;
-import java.util.Arrays;
/**
* A NetlinkMessage subclass for rtnetlink route messages.
@@ -86,18 +81,27 @@
private long mSinceLastUseMillis; // Milliseconds since the route was used,
// for resolved multicast routes
- public RtNetlinkRouteMessage(StructNlMsgHdr header, StructRtMsg rtMsg) {
+
+ @VisibleForTesting
+ public RtNetlinkRouteMessage(final StructNlMsgHdr header, final StructRtMsg rtMsg,
+ final IpPrefix source, final IpPrefix destination, final InetAddress gateway,
+ int iif, int oif, final StructRtaCacheInfo cacheInfo) {
super(header);
mRtmsg = rtMsg;
- mSource = null;
- mDestination = null;
- mGateway = null;
- mIifIndex = 0;
- mOifIndex = 0;
- mRtaCacheInfo = null;
+ mSource = source;
+ mDestination = destination;
+ mGateway = gateway;
+ mIifIndex = iif;
+ mOifIndex = oif;
+ mRtaCacheInfo = cacheInfo;
mSinceLastUseMillis = -1;
}
+ public RtNetlinkRouteMessage(StructNlMsgHdr header, StructRtMsg rtMsg) {
+ this(header, rtMsg, null /* source */, null /* destination */, null /* gateway */,
+ 0 /* iif */, 0 /* oif */, null /* cacheInfo */);
+ }
+
/**
* Returns the rtnetlink family.
*/
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNdOptPio.java b/staticlibs/device/com/android/net/module/util/netlink/StructNdOptPio.java
new file mode 100644
index 0000000..65541eb
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNdOptPio.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import android.net.IpPrefix;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.HexDump;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.PrefixInformationOption;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+/**
+ * The Prefix Information Option. RFC 4861.
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Type | Length | Prefix Length |L|A|R|P| Rsvd1 |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Valid Lifetime |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Preferred Lifetime |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Reserved2 |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | |
+ * + +
+ * | |
+ * + Prefix +
+ * | |
+ * + +
+ * | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class StructNdOptPio extends NdOption {
+ private static final String TAG = StructNdOptPio.class.getSimpleName();
+ public static final int TYPE = 3;
+ public static final byte LENGTH = 4; // Length in 8-byte units
+
+ public final byte flags;
+ public final long preferred;
+ public final long valid;
+ @NonNull
+ public final IpPrefix prefix;
+
+ public StructNdOptPio(byte flags, long preferred, long valid, @NonNull final IpPrefix prefix) {
+ super((byte) TYPE, LENGTH);
+ this.prefix = Objects.requireNonNull(prefix, "prefix must not be null");
+ this.flags = flags;
+ this.preferred = preferred;
+ this.valid = valid;
+ }
+
+ /**
+ * Parses a PrefixInformation option from a {@link ByteBuffer}.
+ *
+ * @param buf The buffer from which to parse the option. The buffer's byte order must be
+ * {@link java.nio.ByteOrder#BIG_ENDIAN}.
+ * @return the parsed option, or {@code null} if the option could not be parsed successfully.
+ */
+ public static StructNdOptPio parse(@NonNull ByteBuffer buf) {
+ if (buf == null || buf.remaining() < LENGTH * 8) return null;
+ try {
+ final PrefixInformationOption pio = Struct.parse(PrefixInformationOption.class, buf);
+ if (pio.type != TYPE) {
+ throw new IllegalArgumentException("Invalid type " + pio.type);
+ }
+ if (pio.length != LENGTH) {
+ throw new IllegalArgumentException("Invalid length " + pio.length);
+ }
+ return new StructNdOptPio(pio.flags, pio.preferredLifetime, pio.validLifetime,
+ pio.getIpPrefix());
+ } catch (IllegalArgumentException | BufferUnderflowException e) {
+ // Not great, but better than throwing an exception that might crash the caller.
+ // Convention in this package is that null indicates that the option was truncated
+ // or malformed, so callers must already handle it.
+ Log.d(TAG, "Invalid PIO option: " + e);
+ return null;
+ }
+ }
+
+ protected void writeToByteBuffer(ByteBuffer buf) {
+ buf.put(PrefixInformationOption.build(prefix, flags, valid, preferred));
+ }
+
+ /** Outputs the wire format of the option to a new big-endian ByteBuffer. */
+ public ByteBuffer toByteBuffer() {
+ final ByteBuffer buf = ByteBuffer.allocate(Struct.getSize(PrefixInformationOption.class));
+ writeToByteBuffer(buf);
+ buf.flip();
+ return buf;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return String.format("NdOptPio(flags:%s, preferred lft:%s, valid lft:%s, prefix:%s)",
+ HexDump.toHexString(flags), preferred, valid, prefix);
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java
index 3cd7292..6d9318c 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.net.module.util.Struct;
import com.android.net.module.util.Struct.Field;
@@ -57,8 +58,9 @@
@Field(order = 8, type = Type.U32)
public final long flags;
- StructRtMsg(short family, short dstLen, short srcLen, short tos, short table, short protocol,
- short scope, short type, long flags) {
+ @VisibleForTesting
+ public StructRtMsg(short family, short dstLen, short srcLen, short tos, short table,
+ short protocol, short scope, short type, long flags) {
this.family = family;
this.dstLen = dstLen;
this.srcLen = srcLen;
diff --git a/staticlibs/device/com/android/net/module/util/structs/FragmentHeader.java b/staticlibs/device/com/android/net/module/util/structs/FragmentHeader.java
new file mode 100644
index 0000000..3da6a38
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/FragmentHeader.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.structs;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/**
+ * IPv6 Fragment Extension header, as per https://tools.ietf.org/html/rfc2460.
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Next Header | Reserved | Fragment Offset |Res|M|
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Identification |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class FragmentHeader extends Struct {
+ @Field(order = 0, type = Type.U8)
+ public final short nextHeader;
+ @Field(order = 1, type = Type.S8)
+ public final byte reserved;
+ @Field(order = 2, type = Type.U16)
+ public final int fragmentOffset;
+ @Field(order = 3, type = Type.S32)
+ public final int identification;
+
+ public FragmentHeader(final short nextHeader, final byte reserved, final int fragmentOffset,
+ final int identification) {
+ this.nextHeader = nextHeader;
+ this.reserved = reserved;
+ this.fragmentOffset = fragmentOffset;
+ this.identification = identification;
+ }
+
+ public FragmentHeader(final short nextHeader, final int fragmentOffset,
+ final int identification) {
+ this(nextHeader, (byte) 0, fragmentOffset, identification);
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/PrefixInformationOption.java b/staticlibs/device/com/android/net/module/util/structs/PrefixInformationOption.java
index 49d7654..bbbe571 100644
--- a/staticlibs/device/com/android/net/module/util/structs/PrefixInformationOption.java
+++ b/staticlibs/device/com/android/net/module/util/structs/PrefixInformationOption.java
@@ -21,11 +21,16 @@
import android.net.IpPrefix;
import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Computed;
import com.android.net.module.util.Struct.Field;
import com.android.net.module.util.Struct.Type;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -71,7 +76,11 @@
@Field(order = 7, type = Type.ByteArray, arraysize = 16)
public final byte[] prefix;
- PrefixInformationOption(final byte type, final byte length, final byte prefixLen,
+ @Computed
+ private final IpPrefix mIpPrefix;
+
+ @VisibleForTesting
+ public PrefixInformationOption(final byte type, final byte length, final byte prefixLen,
final byte flags, final long validLifetime, final long preferredLifetime,
final int reserved, @NonNull final byte[] prefix) {
this.type = type;
@@ -82,6 +91,23 @@
this.preferredLifetime = preferredLifetime;
this.reserved = reserved;
this.prefix = prefix;
+
+ try {
+ final Inet6Address addr = (Inet6Address) InetAddress.getByAddress(prefix);
+ mIpPrefix = new IpPrefix(addr, prefixLen);
+ } catch (UnknownHostException | ClassCastException e) {
+ // UnknownHostException should never happen unless prefix is null.
+ // ClassCastException can occur when prefix is an IPv6 mapped IPv4 address.
+ // Both scenarios should throw an exception in the context of Struct#parse().
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Return the prefix {@link IpPrefix} included in the PIO.
+ */
+ public IpPrefix getIpPrefix() {
+ return mIpPrefix;
}
/**
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/NetworkCapabilitiesUtils.java b/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
index 54ce01e..7066131 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkCapabilitiesUtils.java
@@ -39,12 +39,13 @@
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_SATELLITE;
import static android.net.NetworkCapabilities.TRANSPORT_USB;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
-import static com.android.net.module.util.BitUtils.packBitList;
+import static com.android.net.module.util.BitUtils.packBits;
import static com.android.net.module.util.BitUtils.unpackBits;
import android.annotation.NonNull;
@@ -75,8 +76,8 @@
TRANSPORT_BLUETOOTH,
TRANSPORT_WIFI,
TRANSPORT_ETHERNET,
- TRANSPORT_USB
-
+ TRANSPORT_USB,
+ TRANSPORT_SATELLITE
// Notably, TRANSPORT_TEST is not in this list as any network that has TRANSPORT_TEST and
// one of the above transports should be counted as that transport, to keep tests as
// realistic as possible.
@@ -88,41 +89,41 @@
* and {@code FORCE_RESTRICTED_CAPABILITIES}.
*/
@VisibleForTesting
- public static final long RESTRICTED_CAPABILITIES = packBitList(
- NET_CAPABILITY_BIP,
- NET_CAPABILITY_CBS,
- NET_CAPABILITY_DUN,
- NET_CAPABILITY_EIMS,
- NET_CAPABILITY_ENTERPRISE,
- NET_CAPABILITY_FOTA,
- NET_CAPABILITY_IA,
- NET_CAPABILITY_IMS,
- NET_CAPABILITY_MCX,
- NET_CAPABILITY_RCS,
- NET_CAPABILITY_VEHICLE_INTERNAL,
- NET_CAPABILITY_VSIM,
- NET_CAPABILITY_XCAP,
- NET_CAPABILITY_MMTEL);
+ public static final long RESTRICTED_CAPABILITIES =
+ (1L << NET_CAPABILITY_BIP) |
+ (1L << NET_CAPABILITY_CBS) |
+ (1L << NET_CAPABILITY_DUN) |
+ (1L << NET_CAPABILITY_EIMS) |
+ (1L << NET_CAPABILITY_ENTERPRISE) |
+ (1L << NET_CAPABILITY_FOTA) |
+ (1L << NET_CAPABILITY_IA) |
+ (1L << NET_CAPABILITY_IMS) |
+ (1L << NET_CAPABILITY_MCX) |
+ (1L << NET_CAPABILITY_RCS) |
+ (1L << NET_CAPABILITY_VEHICLE_INTERNAL) |
+ (1L << NET_CAPABILITY_VSIM) |
+ (1L << NET_CAPABILITY_XCAP) |
+ (1L << NET_CAPABILITY_MMTEL);
/**
* Capabilities that force network to be restricted.
* See {@code NetworkCapabilities#maybeMarkCapabilitiesRestricted}.
*/
- private static final long FORCE_RESTRICTED_CAPABILITIES = packBitList(
- NET_CAPABILITY_ENTERPRISE,
- NET_CAPABILITY_OEM_PAID,
- NET_CAPABILITY_OEM_PRIVATE);
+ private static final long FORCE_RESTRICTED_CAPABILITIES =
+ (1L << NET_CAPABILITY_ENTERPRISE) |
+ (1L << NET_CAPABILITY_OEM_PAID) |
+ (1L << NET_CAPABILITY_OEM_PRIVATE);
/**
* Capabilities that suggest that a network is unrestricted.
* See {@code NetworkCapabilities#maybeMarkCapabilitiesRestricted}.
*/
@VisibleForTesting
- public static final long UNRESTRICTED_CAPABILITIES = packBitList(
- NET_CAPABILITY_INTERNET,
- NET_CAPABILITY_MMS,
- NET_CAPABILITY_SUPL,
- NET_CAPABILITY_WIFI_P2P);
+ public static final long UNRESTRICTED_CAPABILITIES =
+ (1L << NET_CAPABILITY_INTERNET) |
+ (1L << NET_CAPABILITY_MMS) |
+ (1L << NET_CAPABILITY_SUPL) |
+ (1L << NET_CAPABILITY_WIFI_P2P);
/**
* Get a transport that can be used to classify a network when displaying its info to users.
@@ -158,28 +159,33 @@
*
* @return {@code true} if the network should be restricted.
*/
- // TODO: Use packBits(nc.getCapabilities()) to check more easily using bit masks.
public static boolean inferRestrictedCapability(NetworkCapabilities nc) {
+ return inferRestrictedCapability(packBits(nc.getCapabilities()));
+ }
+
+ /**
+ * Infers that all the capabilities it provides are typically provided by restricted networks
+ * or not.
+ *
+ * @param capabilities see {@link NetworkCapabilities#getCapabilities()}
+ *
+ * @return {@code true} if the network should be restricted.
+ */
+ public static boolean inferRestrictedCapability(long capabilities) {
// Check if we have any capability that forces the network to be restricted.
- for (int capability : unpackBits(FORCE_RESTRICTED_CAPABILITIES)) {
- if (nc.hasCapability(capability)) {
- return true;
- }
+ if ((capabilities & FORCE_RESTRICTED_CAPABILITIES) != 0) {
+ return true;
}
// Verify there aren't any unrestricted capabilities. If there are we say
// the whole thing is unrestricted unless it is forced to be restricted.
- for (int capability : unpackBits(UNRESTRICTED_CAPABILITIES)) {
- if (nc.hasCapability(capability)) {
- return false;
- }
+ if ((capabilities & UNRESTRICTED_CAPABILITIES) != 0) {
+ return false;
}
// Must have at least some restricted capabilities.
- for (int capability : unpackBits(RESTRICTED_CAPABILITIES)) {
- if (nc.hasCapability(capability)) {
- return true;
- }
+ if ((capabilities & RESTRICTED_CAPABILITIES) != 0) {
+ return true;
}
return false;
}
diff --git a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
index 8315b8f..0d7d96f 100644
--- a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
@@ -23,11 +23,14 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import android.annotation.CheckResult;
import android.annotation.NonNull;
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;
@@ -43,8 +46,9 @@
/**
* Return true if the context has one of given permission.
*/
- public static boolean checkAnyPermissionOf(@NonNull Context context,
- @NonNull String... permissions) {
+ @CheckResult
+ public static boolean hasAnyPermissionOf(@NonNull Context context,
+ @NonNull String... permissions) {
for (String permission : permissions) {
if (context.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
return true;
@@ -54,11 +58,12 @@
}
/**
- * Return true if the context has one of give permission that is allowed
+ * Return true if the context has one of given permission that is allowed
* for a particular process and user ID running in the system.
*/
- public static boolean checkAnyPermissionOf(@NonNull Context context,
- int pid, int uid, @NonNull String... permissions) {
+ @CheckResult
+ public static boolean hasAnyPermissionOf(@NonNull Context context,
+ int pid, int uid, @NonNull String... permissions) {
for (String permission : permissions) {
if (context.checkPermission(permission, pid, uid) == PERMISSION_GRANTED) {
return true;
@@ -72,7 +77,7 @@
*/
public static void enforceAnyPermissionOf(@NonNull Context context,
@NonNull String... permissions) {
- if (!checkAnyPermissionOf(context, permissions)) {
+ if (!hasAnyPermissionOf(context, permissions)) {
throw new SecurityException("Requires one of the following permissions: "
+ String.join(", ", permissions) + ".");
}
@@ -131,7 +136,8 @@
/**
* Return true if the context has DUMP permission.
*/
- public static boolean checkDumpPermission(Context context, String tag, PrintWriter pw) {
+ @CheckResult
+ public static boolean hasDumpPermission(Context context, String tag, PrintWriter pw) {
if (context.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
!= PERMISSION_GRANTED) {
pw.println("Permission Denial: can't dump " + tag + " from from pid="
@@ -183,4 +189,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/lint-baseline.xml b/staticlibs/lint-baseline.xml
new file mode 100644
index 0000000..2ee3a43
--- /dev/null
+++ b/staticlibs/lint-baseline.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha04" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha04">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `makeNetlinkSocketAddress`"
+ errorLine1=" Os.bind(fd, makeNetlinkSocketAddress(0, mBindGroups));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java"
+ line="111"
+ column="25"/>
+ </issue>
+
+</issues>
diff --git a/staticlibs/native/bpf_headers/Android.bp b/staticlibs/native/bpf_headers/Android.bp
index 41184ea..d55584ac 100644
--- a/staticlibs/native/bpf_headers/Android.bp
+++ b/staticlibs/native/bpf_headers/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
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/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index baff09b..dc7925e 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -37,7 +37,26 @@
#define BPFLOADER_IGNORED_ON_VERSION 33u
// Android U / 14 (api level 34) - various new program types added
-#define BPFLOADER_U_VERSION 37u
+#define BPFLOADER_U_VERSION 38u
+
+// Android V / 15 (api level 35) - platform only
+// (note: the platform bpfloader in V isn't really versioned at all,
+// as there is no need as it can only load objects compiled at the
+// same time as itself and the rest of the platform)
+#define BPFLOADER_PLATFORM_VERSION 41u
+
+// Android Mainline - this bpfloader should eventually go back to T (or even S)
+// Note: this value (and the following +1u's) are hardcoded in NetBpfLoad.cpp
+#define BPFLOADER_MAINLINE_VERSION 42u
+
+// Android Mainline BpfLoader when running on Android T
+#define BPFLOADER_MAINLINE_T_VERSION (BPFLOADER_MAINLINE_VERSION + 1u)
+
+// Android Mainline BpfLoader when running on Android U
+#define BPFLOADER_MAINLINE_U_VERSION (BPFLOADER_MAINLINE_T_VERSION + 1u)
+
+// Android Mainline BpfLoader when running on Android V
+#define BPFLOADER_MAINLINE_V_VERSION (BPFLOADER_MAINLINE_U_VERSION + 1u)
/* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
* before #include "bpf_helpers.h" to change which bpfloaders will
@@ -48,7 +67,7 @@
* In which case it's just best to use the default.
*/
#ifndef BPFLOADER_MIN_VER
-#define BPFLOADER_MIN_VER COMPILE_FOR_BPFLOADER_VERSION
+#define BPFLOADER_MIN_VER BPFLOADER_PLATFORM_VERSION
#endif
#ifndef BPFLOADER_MAX_VER
@@ -111,10 +130,12 @@
#define KVER_NONE KVER_(0)
#define KVER_4_14 KVER(4, 14, 0)
#define KVER_4_19 KVER(4, 19, 0)
-#define KVER_5_4 KVER(5, 4, 0)
-#define KVER_5_8 KVER(5, 8, 0)
-#define KVER_5_9 KVER(5, 9, 0)
+#define KVER_5_4 KVER(5, 4, 0)
+#define KVER_5_8 KVER(5, 8, 0)
+#define KVER_5_9 KVER(5, 9, 0)
#define KVER_5_15 KVER(5, 15, 0)
+#define KVER_6_1 KVER(6, 1, 0)
+#define KVER_6_6 KVER(6, 6, 0)
#define KVER_INF KVER_(0xFFFFFFFFu)
#define KVER_IS_AT_LEAST(kver, a, b, c) ((kver).kver >= KVER(a, b, c).kver)
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
index ef03c4d..00ef91a 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -48,9 +48,6 @@
#define DEFAULT_SIZEOF_BPF_MAP_DEF 32 // v0.0 struct: enum (uint sized) + 7 uint
#define DEFAULT_SIZEOF_BPF_PROG_DEF 20 // v0.0 struct: 4 uint + bool + 3 byte alignment pad
-// By default, unless otherwise specified, allow the use of features only supported by v0.37.
-#define COMPILE_FOR_BPFLOADER_VERSION 37u
-
/*
* The bpf_{map,prog}_def structures are compiled for different architectures.
* Once by the BPF compiler for the BPF architecture, and once by a C++
diff --git a/staticlibs/native/bpf_syscall_wrappers/Android.bp b/staticlibs/native/bpf_syscall_wrappers/Android.bp
index b3efc21..1e0cb22 100644
--- a/staticlibs/native/bpf_syscall_wrappers/Android.bp
+++ b/staticlibs/native/bpf_syscall_wrappers/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/native/bpfmapjni/Android.bp b/staticlibs/native/bpfmapjni/Android.bp
index 8babcce..7e6b4ec 100644
--- a/staticlibs/native/bpfmapjni/Android.bp
+++ b/staticlibs/native/bpfmapjni/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/native/bpfutiljni/Android.bp b/staticlibs/native/bpfutiljni/Android.bp
index 39a2795..1ef01a6 100644
--- a/staticlibs/native/bpfutiljni/Android.bp
+++ b/staticlibs/native/bpfutiljni/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/native/ip_checksum/Android.bp b/staticlibs/native/ip_checksum/Android.bp
index 9878d73..e2e118e 100644
--- a/staticlibs/native/ip_checksum/Android.bp
+++ b/staticlibs/native/ip_checksum/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/native/netjniutils/Android.bp b/staticlibs/native/netjniutils/Android.bp
index ca3bbbc..4cab459 100644
--- a/staticlibs/native/netjniutils/Android.bp
+++ b/staticlibs/native/netjniutils/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/native/nettestutils/Android.bp b/staticlibs/native/nettestutils/Android.bp
index df3bb42..ef87f04 100644
--- a/staticlibs/native/nettestutils/Android.bp
+++ b/staticlibs/native/nettestutils/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/native/tcutils/Android.bp b/staticlibs/native/tcutils/Android.bp
index 9a38745..926590d 100644
--- a/staticlibs/native/tcutils/Android.bp
+++ b/staticlibs/native/tcutils/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
index 2b7e620..59ef20d 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/staticlibs/netd/libnetdutils/Android.bp b/staticlibs/netd/libnetdutils/Android.bp
index fdb9380..2ae5911 100644
--- a/staticlibs/netd/libnetdutils/Android.bp
+++ b/staticlibs/netd/libnetdutils/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -22,7 +23,10 @@
"Utils.cpp",
],
defaults: ["netd_defaults"],
- cflags: ["-Wall", "-Werror"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ ],
shared_libs: [
"libbase",
"liblog",
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/Android.bp b/staticlibs/tests/unit/Android.bp
index 0dfca57..fa466f8 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -3,12 +3,16 @@
//########################################################################
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
android_library {
name: "NetworkStaticLibTestsLib",
- srcs: ["src/**/*.java","src/**/*.kt"],
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
min_sdk_version: "30",
defaults: ["framework-connectivity-test-defaults"],
static_libs: [
@@ -21,6 +25,7 @@
"net-utils-device-common-async",
"net-utils-device-common-bpf",
"net-utils-device-common-ip",
+ "net-utils-device-common-struct-base",
"net-utils-device-common-wear",
],
libs: [
@@ -34,8 +39,7 @@
"//packages/modules/NetworkStack/tests/integration",
],
lint: {
- strict_updatability_linting: true,
- test: true
+ test: true,
},
}
@@ -52,5 +56,4 @@
],
jarjar_rules: "jarjar-rules.txt",
test_suites: ["device-tests"],
- lint: { strict_updatability_linting: true },
}
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/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
index 06b3e2f..9fb61d9 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
@@ -17,6 +17,7 @@
package com.android.net.module.util;
import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+import static android.provider.DeviceConfig.NAMESPACE_CAPTIVEPORTALLOGIN;
import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
@@ -71,10 +72,6 @@
public class DeviceConfigUtilsTest {
private static final String TEST_NAME_SPACE = "connectivity";
private static final String TEST_EXPERIMENT_FLAG = "experiment_flag";
- private static final String CORE_NETWORKING_TRUNK_STABLE_NAMESPACE = "android_core_networking";
- private static final String TEST_TRUNK_STABLE_FLAG = "trunk_stable_feature";
- private static final String TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY =
- "com.android.net.flags.trunk_stable_feature";
private static final int TEST_FLAG_VALUE = 28;
private static final String TEST_FLAG_VALUE_STRING = "28";
private static final int TEST_DEFAULT_FLAG_VALUE = 0;
@@ -237,8 +234,12 @@
TEST_EXPERIMENT_FLAG));
doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
TEST_EXPERIMENT_FLAG));
+ doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(
+ NAMESPACE_CAPTIVEPORTALLOGIN, TEST_EXPERIMENT_FLAG));
assertTrue(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
assertTrue(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+ assertTrue(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+ TEST_EXPERIMENT_FLAG));
}
@Test
public void testIsFeatureEnabledFeatureDefaultDisabled() throws Exception {
@@ -246,8 +247,12 @@
TEST_EXPERIMENT_FLAG));
doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
TEST_EXPERIMENT_FLAG));
+ doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_CAPTIVEPORTALLOGIN,
+ TEST_EXPERIMENT_FLAG));
assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+ assertFalse(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+ TEST_EXPERIMENT_FLAG));
// If the flag is unset, package info is not queried
verify(mContext, never()).getPackageManager();
@@ -261,8 +266,12 @@
TEST_EXPERIMENT_FLAG));
doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
TEST_EXPERIMENT_FLAG));
+ doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_CAPTIVEPORTALLOGIN,
+ TEST_EXPERIMENT_FLAG));
assertTrue(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
assertTrue(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+ assertTrue(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+ TEST_EXPERIMENT_FLAG));
// If the feature is force enabled, package info is not queried
verify(mContext, never()).getPackageManager();
@@ -276,8 +285,12 @@
TEST_EXPERIMENT_FLAG));
doReturn("-1").when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
TEST_EXPERIMENT_FLAG));
+ doReturn("-1").when(() -> DeviceConfig.getProperty(NAMESPACE_CAPTIVEPORTALLOGIN,
+ TEST_EXPERIMENT_FLAG));
assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+ assertFalse(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+ TEST_EXPERIMENT_FLAG));
// If the feature is force disabled, package info is not queried
verify(mContext, never()).getPackageManager();
@@ -294,24 +307,36 @@
TEST_EXPERIMENT_FLAG));
doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
TEST_EXPERIMENT_FLAG));
+ doReturn("1").when(() -> DeviceConfig.getProperty(NAMESPACE_CAPTIVEPORTALLOGIN,
+ TEST_EXPERIMENT_FLAG));
assertTrue(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
assertTrue(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+ assertTrue(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+ TEST_EXPERIMENT_FLAG));
// Feature should be disabled by flag value "999999999".
doReturn("999999999").when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
TEST_EXPERIMENT_FLAG));
doReturn("999999999").when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
TEST_EXPERIMENT_FLAG));
+ doReturn("999999999").when(() -> DeviceConfig.getProperty(NAMESPACE_CAPTIVEPORTALLOGIN,
+ TEST_EXPERIMENT_FLAG));
assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+ assertFalse(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+ TEST_EXPERIMENT_FLAG));
// If the flag is not set feature is disabled
doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
TEST_EXPERIMENT_FLAG));
doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
TEST_EXPERIMENT_FLAG));
+ doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_CAPTIVEPORTALLOGIN,
+ TEST_EXPERIMENT_FLAG));
assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+ assertFalse(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+ TEST_EXPERIMENT_FLAG));
}
@Test
@@ -324,9 +349,13 @@
NAMESPACE_CONNECTIVITY, TEST_EXPERIMENT_FLAG));
doReturn("0").when(() -> DeviceConfig.getProperty(
NAMESPACE_TETHERING, TEST_EXPERIMENT_FLAG));
+ doReturn("0").when(() -> DeviceConfig.getProperty(
+ NAMESPACE_CAPTIVEPORTALLOGIN, TEST_EXPERIMENT_FLAG));
assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
+ assertFalse(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+ TEST_EXPERIMENT_FLAG));
doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
TEST_EXPERIMENT_FLAG));
@@ -347,6 +376,21 @@
}
@Test
+ public void testIsCaptivePortalLoginFeatureEnabledCaching() throws Exception {
+ doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(
+ NAMESPACE_CAPTIVEPORTALLOGIN, TEST_EXPERIMENT_FLAG));
+ assertTrue(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+ TEST_EXPERIMENT_FLAG));
+ assertTrue(DeviceConfigUtils.isCaptivePortalLoginFeatureEnabled(mContext,
+ TEST_EXPERIMENT_FLAG));
+
+ // Package info is only queried once
+ verify(mContext, times(1)).getPackageManager();
+ verify(mContext, times(1)).getPackageName();
+ verify(mPm, times(1)).getPackageInfo(anyString(), anyInt());
+ }
+
+ @Test
public void testIsTetheringFeatureEnabledCaching() throws Exception {
doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
TEST_EXPERIMENT_FLAG));
@@ -507,25 +551,4 @@
verify(mContext, never()).getPackageName();
verify(mPm, never()).getPackageInfo(anyString(), anyInt());
}
-
- @Test
- public void testIsCoreNetworkingTrunkStableFeatureEnabled() {
- doReturn(null).when(() -> DeviceConfig.getProperty(
- CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
- TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY));
- assertFalse(DeviceConfigUtils.isTrunkStableFeatureEnabled(
- TEST_TRUNK_STABLE_FLAG));
-
- doReturn("false").when(() -> DeviceConfig.getProperty(
- CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
- TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY));
- assertFalse(DeviceConfigUtils.isTrunkStableFeatureEnabled(
- TEST_TRUNK_STABLE_FLAG));
-
- doReturn("true").when(() -> DeviceConfig.getProperty(
- CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
- TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY));
- assertTrue(DeviceConfigUtils.isTrunkStableFeatureEnabled(
- TEST_TRUNK_STABLE_FLAG));
- }
}
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..8586e82 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,16 +19,18 @@
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
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
-import com.android.net.module.util.PermissionUtils.checkAnyPermissionOf
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.net.module.util.PermissionUtils.hasAnyPermissionOf
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import kotlin.test.assertEquals
@@ -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
@@ -69,18 +78,18 @@
.checkCallingOrSelfPermission(TEST_PERMISSION1)
doReturn(PERMISSION_DENIED).`when`(mockContext)
.checkCallingOrSelfPermission(TEST_PERMISSION2)
- assertTrue(checkAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
+ assertTrue(hasAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
enforceAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2)
doReturn(PERMISSION_DENIED).`when`(mockContext)
.checkCallingOrSelfPermission(TEST_PERMISSION1)
doReturn(PERMISSION_GRANTED).`when`(mockContext)
.checkCallingOrSelfPermission(TEST_PERMISSION2)
- assertTrue(checkAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
+ assertTrue(hasAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
enforceAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2)
doReturn(PERMISSION_DENIED).`when`(mockContext).checkCallingOrSelfPermission(any())
- assertFalse(checkAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
+ assertFalse(hasAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2))
assertFailsWith<SecurityException>("Expect fail but permission granted.") {
enforceAnyPermissionOf(mockContext, TEST_PERMISSION1, TEST_PERMISSION2)
}
@@ -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/NduseroptMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java
index 4fc5ec2..d1df3a6 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java
@@ -252,8 +252,10 @@
public void testToString() {
NduseroptMessage msg = parseNduseroptMessage(toBuffer(MSG_PREF64));
assertNotNull(msg);
- assertEquals("Nduseroptmsg(10, 16, 1431655765, 134, 0, fe80:2:3:4:5:6:7:8%1431655765)",
- msg.toString());
+ final String expected = "Nduseroptmsg(family:10, opts_len:16, ifindex:1431655765, "
+ + "icmp_type:134, icmp_code:0, srcaddr: fe80:2:3:4:5:6:7:8%1431655765, "
+ + "NdOptPref64(2001:db8:3:4:5:6::/96, 10064))";
+ assertEquals(expected, msg.toString());
}
// Convenience method to parse a NduseroptMessage that's not part of a netlink message.
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 0958f11..f64adb8 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,6 +21,8 @@
import static android.system.OsConstants.AF_UNSPEC;
import static android.system.OsConstants.EACCES;
import static android.system.OsConstants.NETLINK_ROUTE;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_RCVBUF;
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;
@@ -33,6 +35,8 @@
import static org.junit.Assume.assumeFalse;
import android.content.Context;
+import android.net.util.SocketUtils;
+import android.os.Build;
import android.system.ErrnoException;
import android.system.NetlinkSocketAddress;
import android.system.Os;
@@ -43,6 +47,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.Struct;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import libcore.io.IoUtils;
@@ -204,4 +209,23 @@
assertNotNull("Route doesn't contain destination: " + route, route.getDestination());
}
}
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R) // getsockoptInt requires > R
+ public void testNetlinkSocketForProto_defaultBufferSize() throws Exception {
+ final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
+ final int bufferSize = Os.getsockoptInt(fd, SOL_SOCKET, SO_RCVBUF) / 2;
+
+ assertTrue("bufferSize: " + bufferSize, bufferSize > 0); // whatever the default value is
+ SocketUtils.closeSocket(fd);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R) // getsockoptInt requires > R
+ public void testNetlinkSocketForProto_setBufferSize() throws Exception {
+ final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE,
+ 8000);
+ final int bufferSize = Os.getsockoptInt(fd, SOL_SOCKET, SO_RCVBUF) / 2;
+
+ assertEquals(8000, bufferSize);
+ SocketUtils.closeSocket(fd);
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java
index 01126d2..1d08525 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java
@@ -42,6 +42,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.net.Inet4Address;
import java.net.Inet6Address;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -179,6 +180,57 @@
}
@Test
+ public void testCreateRtmNewAddressMessage_IPv4Address() {
+ // Hexadecimal representation of our created packet.
+ final String expectedNewAddressHex =
+ // struct nlmsghdr
+ "4c000000" // length = 76
+ + "1400" // type = 20 (RTM_NEWADDR)
+ + "0501" // flags = NLM_F_ACK | NLM_F_REQUEST | NLM_F_REPLACE
+ + "01000000" // seqno = 1
+ + "00000000" // pid = 0 (send to kernel)
+ // struct IfaddrMsg
+ + "02" // family = inet
+ + "18" // prefix len = 24
+ + "00" // flags = 0
+ + "00" // scope = RT_SCOPE_UNIVERSE
+ + "14000000" // ifindex = 20
+ // struct nlattr: IFA_ADDRESS
+ + "0800" // len
+ + "0100" // type
+ + "C0A80491" // IPv4 address = 192.168.4.145
+ // struct nlattr: IFA_CACHEINFO
+ + "1400" // len
+ + "0600" // type
+ + "C0A80000" // preferred = 43200s
+ + "C0A80000" // valid = 43200s
+ + "00000000" // cstamp
+ + "00000000" // tstamp
+ // struct nlattr: IFA_FLAGS
+ + "0800" // len
+ + "0800" // type
+ + "00000000" // flags = 0
+ // struct nlattr: IFA_LOCAL
+ + "0800" // len
+ + "0200" // type
+ + "C0A80491" // local address = 192.168.4.145
+ // struct nlattr: IFA_BROADCAST
+ + "0800" // len
+ + "0400" // type
+ + "C0A804FF"; // broadcast address = 192.168.4.255
+ final byte[] expectedNewAddress =
+ HexEncoding.decode(expectedNewAddressHex.toCharArray(), false);
+
+ final Inet4Address ipAddress =
+ (Inet4Address) InetAddresses.parseNumericAddress("192.168.4.145");
+ final byte[] bytes = RtNetlinkAddressMessage.newRtmNewAddressMessage(1 /* seqno */,
+ ipAddress, (short) 24 /* prefix len */, 0 /* flags */,
+ (byte) RT_SCOPE_UNIVERSE /* scope */, 20 /* ifindex */,
+ (long) 0xA8C0 /* preferred */, (long) 0xA8C0 /* valid */);
+ assertArrayEquals(expectedNewAddress, bytes);
+ }
+
+ @Test
public void testCreateRtmDelAddressMessage() {
// Hexadecimal representation of our created packet.
final String expectedDelAddressHex =
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNdOptPioTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNdOptPioTest.java
new file mode 100644
index 0000000..0d88829
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNdOptPioTest.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import static com.android.net.module.util.NetworkStackConstants.INFINITE_LEASE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.net.IpPrefix;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.structs.PrefixInformationOption;
+
+import libcore.util.HexEncoding;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructNdOptPioTest {
+ private static final IpPrefix TEST_PREFIX = new IpPrefix("2a00:79e1:abc:f605::/64");
+ private static final byte TEST_PIO_FLAGS_P_UNSET = (byte) 0xC0; // L=1,A=1
+ private static final byte TEST_PIO_FLAGS_P_SET = (byte) 0xD0; // L=1,A=1,P=1
+ private static final String PIO_BYTES =
+ "0304" // type=3, length=4
+ + "40" // prefix length=64
+ + "C0" // L=1,A=1
+ + "00278D00" // valid=259200
+ + "00093A80" // preferred=604800
+ + "00000000" // Reserved2
+ + "2A0079E10ABCF6050000000000000000"; // prefix=2a00:79e1:abc:f605::
+
+ private static final String PIO_WITH_P_FLAG_BYTES =
+ "0304" // type=3, length=4
+ + "40" // prefix length=64
+ + "D0" // L=1,A=1,P=1
+ + "00278D00" // valid=2592000
+ + "00093A80" // preferred=604800
+ + "00000000" // Reserved2
+ + "2A0079E10ABCF6050000000000000000"; // prefix=2a00:79e1:abc:f605::
+
+ private static final String PIO_WITH_P_FLAG_INFINITY_LIFETIME_BYTES =
+ "0304" // type=3, length=4
+ + "40" // prefix length=64
+ + "D0" // L=1,A=1,P=1
+ + "FFFFFFFF" // valid=infinity
+ + "FFFFFFFF" // preferred=infintiy
+ + "00000000" // Reserved2
+ + "2A0079E10ABCF6050000000000000000"; // prefix=2a00:79e1:abc:f605::
+
+ private static void assertPioOptMatches(final StructNdOptPio opt, int length, byte flags,
+ long preferred, long valid, final IpPrefix prefix) {
+ assertEquals(StructNdOptPio.TYPE, opt.type);
+ assertEquals(length, opt.length);
+ assertEquals(flags, opt.flags);
+ assertEquals(preferred, opt.preferred);
+ assertEquals(valid, opt.valid);
+ assertEquals(prefix, opt.prefix);
+ }
+
+ private static void assertToByteBufferMatches(final StructNdOptPio opt, final String expected) {
+ String actual = HexEncoding.encodeToString(opt.toByteBuffer().array());
+ assertEquals(expected, actual);
+ }
+
+ private static void doPioParsingTest(final String optionHexString, int length, byte flags,
+ long preferred, long valid, final IpPrefix prefix) {
+ final byte[] rawBytes = HexEncoding.decode(optionHexString);
+ final StructNdOptPio opt = StructNdOptPio.parse(ByteBuffer.wrap(rawBytes));
+ assertPioOptMatches(opt, length, flags, preferred, valid, prefix);
+ assertToByteBufferMatches(opt, optionHexString);
+ }
+
+ @Test
+ public void testParsingPioWithoutPFlag() {
+ doPioParsingTest(PIO_BYTES, 4 /* length */, TEST_PIO_FLAGS_P_UNSET,
+ 604800 /* preferred */, 2592000 /* valid */, TEST_PREFIX);
+ }
+
+ @Test
+ public void testParsingPioWithPFlag() {
+ doPioParsingTest(PIO_WITH_P_FLAG_BYTES, 4 /* length */, TEST_PIO_FLAGS_P_SET,
+ 604800 /* preferred */, 2592000 /* valid */, TEST_PREFIX);
+ }
+
+ @Test
+ public void testParsingPioWithPFlag_infinityLifetime() {
+ doPioParsingTest(PIO_WITH_P_FLAG_INFINITY_LIFETIME_BYTES, 4 /* length */,
+ TEST_PIO_FLAGS_P_SET,
+ Integer.toUnsignedLong(INFINITE_LEASE) /* preferred */,
+ Integer.toUnsignedLong(INFINITE_LEASE) /* valid */,
+ TEST_PREFIX);
+ }
+
+ @Test
+ public void testToByteBuffer() {
+ final StructNdOptPio pio =
+ new StructNdOptPio(TEST_PIO_FLAGS_P_UNSET, 604800 /* preferred */,
+ 2592000 /* valid */, TEST_PREFIX);
+ assertToByteBufferMatches(pio, PIO_BYTES);
+ }
+
+ @Test
+ public void testToByteBuffer_withPFlag() {
+ final StructNdOptPio pio =
+ new StructNdOptPio(TEST_PIO_FLAGS_P_SET, 604800 /* preferred */,
+ 2592000 /* valid */, TEST_PREFIX);
+ assertToByteBufferMatches(pio, PIO_WITH_P_FLAG_BYTES);
+ }
+
+ @Test
+ public void testToByteBuffer_infinityLifetime() {
+ final StructNdOptPio pio =
+ new StructNdOptPio(TEST_PIO_FLAGS_P_SET,
+ Integer.toUnsignedLong(INFINITE_LEASE) /* preferred */,
+ Integer.toUnsignedLong(INFINITE_LEASE) /* valid */, TEST_PREFIX);
+ assertToByteBufferMatches(pio, PIO_WITH_P_FLAG_INFINITY_LIFETIME_BYTES);
+ }
+
+ private static ByteBuffer makePioOption(byte type, byte length, byte prefixLen, byte flags,
+ long valid, long preferred, final byte[] prefix) {
+ final PrefixInformationOption pio = new PrefixInformationOption(type, length, prefixLen,
+ flags, valid, preferred, 0 /* reserved */, prefix);
+ return ByteBuffer.wrap(pio.writeToBytes(ByteOrder.BIG_ENDIAN));
+ }
+
+ @Test
+ public void testParsing_invalidOptionType() {
+ final ByteBuffer buf = makePioOption((byte) 24 /* wrong type:RIO */,
+ (byte) 4 /* length */, (byte) 64 /* prefixLen */, TEST_PIO_FLAGS_P_SET,
+ 2592000 /* valid */, 604800 /* preferred */, TEST_PREFIX.getRawAddress());
+ assertNull(StructNdOptPio.parse(buf));
+ }
+
+ @Test
+ public void testParsing_invalidOptionLength() {
+ final ByteBuffer buf = makePioOption((byte) 24 /* wrong type:RIO */,
+ (byte) 3 /* wrong length */, (byte) 64 /* prefixLen */,
+ TEST_PIO_FLAGS_P_SET, 2592000 /* valid */, 604800 /* preferred */,
+ TEST_PREFIX.getRawAddress());
+ assertNull(StructNdOptPio.parse(buf));
+ }
+
+ @Test
+ public void testParsing_truncatedByteBuffer() {
+ final ByteBuffer buf = makePioOption((byte) 3 /* type */, (byte) 4 /* length */,
+ (byte) 64 /* prefixLen */, TEST_PIO_FLAGS_P_SET,
+ 2592000 /* valid */, 604800 /* preferred */, TEST_PREFIX.getRawAddress());
+ final int len = buf.limit();
+ for (int i = 0; i < buf.limit() - 1; i++) {
+ buf.flip();
+ buf.limit(i);
+ assertNull("Option truncated to " + i + " bytes, should have returned null",
+ StructNdOptPio.parse(buf));
+ }
+ buf.flip();
+ buf.limit(len);
+
+ final StructNdOptPio opt = StructNdOptPio.parse(buf);
+ assertPioOptMatches(opt, (byte) 4 /* length */, TEST_PIO_FLAGS_P_SET,
+ 604800 /* preferred */, 2592000 /* valid */, TEST_PREFIX);
+ }
+
+ @Test
+ public void testParsing_invalidByteBufferLength() {
+ final ByteBuffer buf = makePioOption((byte) 3 /* type */, (byte) 4 /* length */,
+ (byte) 64 /* prefixLen */, TEST_PIO_FLAGS_P_SET,
+ 2592000 /* valid */, 604800 /* preferred */, TEST_PREFIX.getRawAddress());
+ buf.limit(31); // less than 4 * 8
+ assertNull(StructNdOptPio.parse(buf));
+ }
+
+ @Test
+ public void testToString() {
+ final ByteBuffer buf = makePioOption((byte) 3 /* type */, (byte) 4 /* length */,
+ (byte) 64 /* prefixLen */, TEST_PIO_FLAGS_P_SET,
+ 2592000 /* valid */, 604800 /* preferred */, TEST_PREFIX.getRawAddress());
+ final StructNdOptPio opt = StructNdOptPio.parse(buf);
+ final String expected = "NdOptPio"
+ + "(flags:D0, preferred lft:604800, valid lft:2592000,"
+ + " prefix:2a00:79e1:abc:f605::/64)";
+ assertEquals(expected, opt.toString());
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/FragmentHeaderTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/FragmentHeaderTest.java
new file mode 100644
index 0000000..1a78ca5
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/FragmentHeaderTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.structs;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+public class FragmentHeaderTest {
+ private static final byte[] HEADER_BYTES = new byte[] {
+ 17, /* nextHeader */
+ 0, /* reserved */
+ 15, 1, /* fragmentOffset */
+ 1, 2, 3, 4 /* identification */
+ };
+
+ @Test
+ public void testConstructor() {
+ FragmentHeader fragHdr = new FragmentHeader((short) 10 /* nextHeader */,
+ (byte) 11 /* reserved */,
+ 12 /* fragmentOffset */,
+ 13 /* identification */);
+
+ assertEquals(10, fragHdr.nextHeader);
+ assertEquals(11, fragHdr.reserved);
+ assertEquals(12, fragHdr.fragmentOffset);
+ assertEquals(13, fragHdr.identification);
+ }
+
+ @Test
+ public void testParseFragmentHeader() {
+ final ByteBuffer buf = ByteBuffer.wrap(HEADER_BYTES);
+ buf.order(ByteOrder.BIG_ENDIAN);
+ FragmentHeader fragHdr = FragmentHeader.parse(FragmentHeader.class, buf);
+
+ assertEquals(17, fragHdr.nextHeader);
+ assertEquals(0, fragHdr.reserved);
+ assertEquals(0xF01, fragHdr.fragmentOffset);
+ assertEquals(0x1020304, fragHdr.identification);
+ }
+
+ @Test
+ public void testWriteToBytes() {
+ FragmentHeader fragHdr = new FragmentHeader((short) 17 /* nextHeader */,
+ (byte) 0 /* reserved */,
+ 0xF01 /* fragmentOffset */,
+ 0x1020304 /* identification */);
+
+ byte[] bytes = fragHdr.writeToBytes(ByteOrder.BIG_ENDIAN);
+
+ assertArrayEquals("bytes = " + Arrays.toString(bytes), HEADER_BYTES, bytes);
+ }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index a5c4fea..9124ac0 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -24,11 +25,11 @@
],
defaults: [
"framework-connectivity-test-defaults",
- "lib_mockito_extended"
+ "lib_mockito_extended",
],
libs: [
"androidx.annotation_annotation",
- "net-utils-device-common-bpf", // TestBpfMap extends IBpfMap.
+ "net-utils-device-common-bpf", // TestBpfMap extends IBpfMap.
],
static_libs: [
"androidx.test.ext.junit",
@@ -39,10 +40,13 @@
"net-utils-device-common-async",
"net-utils-device-common-netlink",
"net-utils-device-common-struct",
+ "net-utils-device-common-struct-base",
"net-utils-device-common-wear",
"modules-utils-build_system",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
}
java_library {
@@ -72,9 +76,11 @@
"jsr305",
],
static_libs: [
- "kotlin-test"
+ "kotlin-test",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
}
java_test_host {
@@ -91,6 +97,8 @@
"cts",
"mts-networking",
"mcts-networking",
+ "mts-tethering",
+ "mcts-tethering",
],
data: [":ConnectivityTestPreparer"],
}
diff --git a/staticlibs/testutils/app/connectivitychecker/Android.bp b/staticlibs/testutils/app/connectivitychecker/Android.bp
index 049ec9e..394c6be 100644
--- a/staticlibs/testutils/app/connectivitychecker/Android.bp
+++ b/staticlibs/testutils/app/connectivitychecker/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -29,6 +30,7 @@
"modules-utils-build_system",
"net-tests-utils",
],
- host_required: ["net-tests-utils-host-common"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
}
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/AutoReleaseNetworkCallbackRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt
new file mode 100644
index 0000000..28ae609
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.RecorderCallback.CallbackEntry
+import java.util.Collections
+import kotlin.test.fail
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * A rule to file [NetworkCallback]s to request or watch networks.
+ *
+ * The callbacks filed in test methods are automatically unregistered when the method completes.
+ */
+class AutoReleaseNetworkCallbackRule : NetworkCallbackHelper(), TestRule {
+ override fun apply(base: Statement, description: Description): Statement {
+ return RequestCellNetworkStatement(base, description)
+ }
+
+ private inner class RequestCellNetworkStatement(
+ private val base: Statement,
+ private val description: Description
+ ) : Statement() {
+ override fun evaluate() {
+ tryTest {
+ base.evaluate()
+ } cleanup {
+ unregisterAll()
+ }
+ }
+ }
+}
+
+/**
+ * Helps file [NetworkCallback]s to request or watch networks, keeping track of them for cleanup.
+ */
+open class NetworkCallbackHelper {
+ private val cm by lazy {
+ InstrumentationRegistry.getInstrumentation().context
+ .getSystemService(ConnectivityManager::class.java)
+ ?: fail("ConnectivityManager not found")
+ }
+ private val cbToCleanup = Collections.synchronizedSet(mutableSetOf<NetworkCallback>())
+ private var cellRequestCb: TestableNetworkCallback? = null
+
+ /**
+ * Convenience method to request a cell network, similarly to [requestNetwork].
+ *
+ * The rule will keep tract of a single cell network request, which can be unrequested manually
+ * using [unrequestCell].
+ */
+ fun requestCell(): Network {
+ if (cellRequestCb != null) {
+ fail("Cell network was already requested")
+ }
+ val cb = requestNetwork(
+ NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build()
+ )
+ cellRequestCb = cb
+ return cb.expect<CallbackEntry.Available>(
+ errorMsg = "Cell network not available. " +
+ "Please ensure the device has working mobile data."
+ ).network
+ }
+
+ /**
+ * Unrequest a cell network requested through [requestCell].
+ */
+ fun unrequestCell() {
+ val cb = cellRequestCb ?: fail("Cell network was not requested")
+ unregisterNetworkCallback(cb)
+ cellRequestCb = null
+ }
+
+ /**
+ * File a request for a Network.
+ *
+ * This will fail tests (throw) if the cell network cannot be obtained, or if it was already
+ * requested.
+ *
+ * Tests may call [unregisterNetworkCallback] once they are done using the returned [Network],
+ * otherwise it will be automatically unrequested after the test.
+ */
+ @JvmOverloads
+ fun requestNetwork(
+ request: NetworkRequest,
+ cb: TestableNetworkCallback = TestableNetworkCallback()
+ ): TestableNetworkCallback {
+ cm.requestNetwork(request, cb)
+ cbToCleanup.add(cb)
+ return cb
+ }
+
+ /**
+ * File a callback for a NetworkRequest.
+ *
+ * This will fail tests (throw) if the cell network cannot be obtained, or if it was already
+ * requested.
+ *
+ * Tests may call [unregisterNetworkCallback] once they are done using the returned [Network],
+ * otherwise it will be automatically unrequested after the test.
+ */
+ @JvmOverloads
+ fun registerNetworkCallback(
+ request: NetworkRequest,
+ cb: TestableNetworkCallback = TestableNetworkCallback()
+ ): TestableNetworkCallback {
+ cm.registerNetworkCallback(request, cb)
+ cbToCleanup.add(cb)
+ return cb
+ }
+
+ /**
+ * Unregister a callback filed using registration methods in this class.
+ */
+ fun unregisterNetworkCallback(cb: NetworkCallback) {
+ cm.unregisterNetworkCallback(cb)
+ cbToCleanup.remove(cb)
+ }
+
+ /**
+ * Unregister all callbacks that were filed using registration methods in this class.
+ */
+ fun unregisterAll() {
+ cbToCleanup.forEach { cm.unregisterNetworkCallback(it) }
+ cbToCleanup.clear()
+ cellRequestCb = null
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
index b1d64f8..8090d5b 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -116,7 +116,10 @@
}
}
- private fun connectToWifiConfig(config: WifiConfiguration) {
+ // Suppress warning because WifiManager methods to connect to a config are
+ // documented not to be deprecated for privileged users.
+ @Suppress("DEPRECATION")
+ fun connectToWifiConfig(config: WifiConfiguration) {
repeat(MAX_WIFI_CONNECT_RETRIES) {
val error = runAsShell(permission.NETWORK_SETTINGS) {
val listener = ConnectWifiListener()
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
index 10accd4..69fdbf8 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -31,6 +31,7 @@
import org.junit.runner.notification.Failure
import org.junit.runner.notification.RunNotifier
import org.junit.runners.Parameterized
+import org.mockito.Mockito
/**
* A runner that can skip tests based on the development SDK as defined in [DevSdkIgnoreRule].
@@ -124,6 +125,9 @@
notifier.fireTestFailure(Failure(leakMonitorDesc,
IllegalStateException("Unexpected thread changes: $threadsDiff")))
}
+ // Clears up internal state of all inline mocks.
+ // TODO: Call clearInlineMocks() at the end of each test.
+ Mockito.framework().clearInlineMocks()
notifier.fireTestFinished(leakMonitorDesc)
}
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/ExternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/ExternalPacketForwarder.kt
new file mode 100644
index 0000000..36eb795
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ExternalPacketForwarder.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.testutils
+
+import java.io.FileDescriptor
+
+class ExternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ forwardMap: Map<Int, Int>
+) : PacketForwarderBase(srcFd, mtu, dstFd, forwardMap) {
+
+ /**
+ * Prepares a packet for forwarding by potentially updating the
+ * source port based on the specified port remapping rules.
+ *
+ * @param buf The packet data as a byte array.
+ * @param version The IP version of the packet (e.g., 4 for IPv4).
+ */
+ override fun remapPort(buf: ByteArray, version: Int) {
+ val transportOffset = getTransportOffset(version)
+ val intPort = getRemappedPort(buf, transportOffset)
+
+ // Copy remapped source port.
+ if (intPort != 0) {
+ setPortAt(intPort, buf, transportOffset)
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/InternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/InternalPacketForwarder.kt
new file mode 100644
index 0000000..58829dc
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/InternalPacketForwarder.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.testutils
+
+import java.io.FileDescriptor
+
+class InternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ forwardMap: Map<Int, Int>
+) : PacketForwarderBase(srcFd, mtu, dstFd, forwardMap) {
+ /**
+ * Prepares a packet for forwarding by potentially updating the
+ * destination port based on the specified port remapping rules.
+ *
+ * @param buf The packet data as a byte array.
+ * @param version The IP version of the packet (e.g., 4 for IPv4).
+ */
+ override fun remapPort(buf: ByteArray, version: Int) {
+ val transportOffset = getTransportOffset(version) + DESTINATION_PORT_OFFSET
+ val extPort = getRemappedPort(buf, transportOffset)
+
+ // Copy remapped destination port.
+ if (extPort != 0) {
+ setPortAt(extPort, buf, transportOffset)
+ }
+ }
+}
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..0b736d1 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,61 +32,65 @@
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,
- dnsAddr: InetAddress
+ addresses: List<LinkAddress>,
+ dnsAddr: InetAddress,
+ portMapping: List<Pair<Int, Int>>
) {
- 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
private val externalNetworkCallback: TestableNetworkCallback
+
+ private val internalForwardMap = HashMap<Int, Int>()
+ private val externalForwardMap = HashMap<Int, Int>()
+
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
+ for (mapping in portMapping) {
+ internalForwardMap[mapping.first] = mapping.second
+ externalForwardMap[mapping.second] = mapping.first
+ }
}
- // 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(
+ private val pr1 = InternalPacketForwarder(
internalFd,
1500,
externalFd,
- externalAddr.address,
- natMap
+ internalForwardMap
)
- private val pr2 = NatExternalPacketForwarder(
+ private val pr2 = ExternalPacketForwarder(
externalFd,
1500,
internalFd,
- externalAddr.address,
- natMap
+ externalForwardMap
)
fun start() {
@@ -107,7 +112,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 +125,7 @@
cm.requestNetwork(nr, testCb)
val lp = LinkProperties().apply {
- addLinkAddress(addr)
+ setLinkAddresses(addresses)
interfaceName = testIface.interfaceName
addDnsServer(dnsAddr)
}
@@ -130,44 +135,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/PacketForwarderBase.java
similarity index 63%
rename from staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
rename to staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
index 0a2b5d4..5c79eb0 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketForwarderBase.java
@@ -30,16 +30,14 @@
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.Map;
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,29 +52,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";
+public abstract class PacketForwarderBase extends Thread {
+ private static final String TAG = "PacketForwarder";
static final int DESTINATION_PORT_OFFSET = 2;
// The source fd to read packets from.
@@ -88,27 +71,14 @@
// 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;
+ @NonNull
+ final Map<Integer, Integer> mPortRemapRules;
/**
- * Construct a {@link NatPacketForwarderBase}.
+ * Construct a {@link PacketForwarderBase}.
*
* 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,28 +86,50 @@
* @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.
+ * @param portRemapRules port remap rules
*/
- public NatPacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
- @NonNull FileDescriptor dstFd, @NonNull InetAddress extAddr,
- @NonNull PacketBridge.NatMap natMap) {
+ public PacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
+ @NonNull FileDescriptor dstFd,
+ @NonNull Map<Integer, Integer> portRemapRules) {
super(TAG);
mSrcFd = Objects.requireNonNull(srcFd);
mBuf = new byte[mtu];
mDstFd = Objects.requireNonNull(dstFd);
- mExtAddr = Objects.requireNonNull(extAddr);
- mNatMap = Objects.requireNonNull(natMap);
+ mPortRemapRules = Objects.requireNonNull(portRemapRules);
}
/**
* 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.
+ * which includes ports mapping.
+ * Subclasses should override this method to implement the needed port remapping.
+ * For internal forwarder will remapped destination port,
+ * external forwarder will remapped source port.
+ * Example:
+ * An outgoing packet from the internal interface with
+ * source 1.2.3.4:1234 and destination 8.8.8.8:80
+ * might be translated to 8.8.8.8:1234 -> 1.2.3.4:8080 before forwarding.
+ * An outgoing packet from the external interface with
+ * source 1.2.3.4:8080 and destination 8.8.8.8:1234
+ * might be translated to 8.8.8.8:80 -> 1.2.3.4:1234 before forwarding.
*/
- abstract void preparePacketForForwarding(@NonNull byte[] buf, int len, int version, int proto);
+ abstract void remapPort(@NonNull byte[] buf, int version);
+
+ /**
+ * Retrieves a potentially remapped port number from a packet.
+ *
+ * @param buf The packet data as a byte array.
+ * @param transportOffset The offset within the packet where the transport layer port begins.
+ * @return The remapped port if a mapping exists in the internal forwarding map,
+ * otherwise returns 0 (indicating no remapping).
+ */
+ int getRemappedPort(@NonNull byte[] buf, int transportOffset) {
+ int port = PacketReflectorUtil.getPortAt(buf, transportOffset);
+ return mPortRemapRules.getOrDefault(port, 0);
+ }
+
+ int getTransportOffset(int version) {
+ return version == 4 ? IPV4_HEADER_LENGTH : IPV6_HEADER_LENGTH;
+ }
private void forwardPacket(@NonNull byte[] buf, int len) {
try {
@@ -147,7 +139,13 @@
}
}
- // Reads one packet from mSrcFd, and writes the packet to the mDstFd for supported protocols.
+ /**
+ * Reads one packet from mSrcFd, and writes the packet to the mDestFd for supported protocols.
+ * This includes:
+ * 1.Address Swapping: Swaps source and destination IP addresses.
+ * 2.Port Remapping: Remap port if necessary.
+ * 3.Checksum Recalculation: Updates IP and transport layer checksums to reflect changes.
+ */
private void processPacket() {
final int len = PacketReflectorUtil.readPacket(mSrcFd, mBuf);
if (len < 1) {
@@ -190,12 +188,19 @@
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 source and destination address.
+ PacketReflectorUtil.swapAddresses(mBuf, version);
+
+ // Remapping the port.
+ remapPort(mBuf, version);
+
+ // Fix IP and Transport layer checksum.
+ PacketReflectorUtil.fixPacketChecksum(mBuf, len, version, proto);
+
// Send the packet to the destination fd.
forwardPacket(mBuf, len);
}
-
@Override
public void run() {
Log.i(TAG, "starting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
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/devicetests/com/android/testutils/TestHttpServer.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
index 740bf63..f1f0c1c 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
@@ -25,8 +25,10 @@
* A minimal HTTP server running on a random available port.
*
* @param host The host to listen to, or null to listen on all hosts
+ * @param port The port to listen to, or 0 to auto select
*/
-class TestHttpServer(host: String? = null) : NanoHTTPD(host, 0 /* auto-select the port */) {
+class TestHttpServer
+ @JvmOverloads constructor(host: String? = null, port: Int = 0) : NanoHTTPD(host, port) {
// Map of URL path -> HTTP response code
private val responses = HashMap<Request, Response>()
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt b/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt
index af4f96d..c6e5f25 100644
--- a/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt
@@ -19,10 +19,77 @@
package com.android.testutils
import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutorService
import java.util.concurrent.TimeUnit
+import java.util.function.Consumer
import kotlin.system.measureTimeMillis
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
// For Java usage
fun durationOf(fn: Runnable) = measureTimeMillis { fn.run() }
fun CountDownLatch.await(timeoutMs: Long): Boolean = await(timeoutMs, TimeUnit.MILLISECONDS)
+
+/**
+ * Quit resources provided as a list by a supplier.
+ *
+ * The supplier may return more resources as the process progresses, for example while interrupting
+ * threads and waiting for them to finish they may spawn more threads, so this implements a
+ * [maxRetryCount] which, in this case, would be the maximum length of the thread chain that can be
+ * terminated.
+ */
+fun <T> quitResources(
+ maxRetryCount: Int,
+ supplier: () -> List<T>,
+ terminator: Consumer<T>
+) {
+ // Run it multiple times since new threads might be generated in a thread
+ // that is about to be terminated
+ for (retryCount in 0 until maxRetryCount) {
+ val resourcesToBeCleared = supplier()
+ if (resourcesToBeCleared.isEmpty()) return
+ for (resource in resourcesToBeCleared) {
+ terminator.accept(resource)
+ }
+ }
+ assertEmpty(supplier())
+}
+
+/**
+ * Implementation of [quitResources] to interrupt and wait for [ExecutorService]s to finish.
+ */
+@JvmOverloads
+fun quitExecutorServices(
+ maxRetryCount: Int,
+ interrupt: Boolean = true,
+ timeoutMs: Long = 10_000L,
+ supplier: () -> List<ExecutorService>
+) {
+ quitResources(maxRetryCount, supplier) { ecs ->
+ if (interrupt) {
+ ecs.shutdownNow()
+ }
+ assertTrue(ecs.awaitTermination(timeoutMs, TimeUnit.MILLISECONDS),
+ "ExecutorServices did not terminate within timeout")
+ }
+}
+
+/**
+ * Implementation of [quitResources] to interrupt and wait for [Thread]s to finish.
+ */
+@JvmOverloads
+fun quitThreads(
+ maxRetryCount: Int,
+ interrupt: Boolean = true,
+ timeoutMs: Long = 10_000L,
+ supplier: () -> List<Thread>
+) {
+ quitResources(maxRetryCount, supplier) { th ->
+ if (interrupt) {
+ th.interrupt()
+ }
+ th.join(timeoutMs)
+ assertFalse(th.isAlive, "Threads did not terminate within timeout.")
+ }
+}
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/Android.bp b/tests/benchmark/Android.bp
index 6ea5347..7854bb5 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -40,4 +41,3 @@
test_suites: ["device-tests"],
jarjar_rules: ":connectivity-jarjar-rules",
}
-
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 7b5c298..6e9d614 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -17,6 +17,7 @@
// Tests in this folder are included both in unit tests and CTS.
// They must be fast and stable, and exercise public or test APIs.
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -93,7 +94,10 @@
name: "ConnectivityCoverageTests",
// Tethering started on SDK 30
min_sdk_version: "30",
- test_suites: ["general-tests", "mts-tethering"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
defaults: [
"ConnectivityTestsLatestSdkDefaults",
"framework-connectivity-internal-test-defaults",
@@ -185,7 +189,7 @@
// See SuiteModuleLoader.java.
// TODO: why are the modules separated by + instead of being separate entries in the array?
mainline_presubmit_modules = [
- "CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex",
+ "CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex",
]
cc_defaults {
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 3a3459b..6eb56c7b 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -54,13 +54,13 @@
import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_SATELLITE;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_USB;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
import static android.os.Process.INVALID_UID;
-
import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
import static com.android.modules.utils.build.SdkLevel.isAtLeastV;
@@ -68,7 +68,6 @@
import static com.android.testutils.MiscAsserts.assertEmpty;
import static com.android.testutils.MiscAsserts.assertThrows;
import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
-
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -82,25 +81,22 @@
import android.net.wifi.aware.PeerHandle;
import android.net.wifi.aware.WifiAwareNetworkSpecifier;
import android.os.Build;
-import android.test.suitebuilder.annotation.SmallTest;
import android.util.ArraySet;
import android.util.Range;
-
+import androidx.test.filters.SmallTest;
import com.android.testutils.CompatUtil;
import com.android.testutils.ConnectivityModuleTest;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mockito;
-
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
@SmallTest
@RunWith(DevSdkIgnoreRunner.class)
@@ -761,6 +757,47 @@
}
@Test
+ public void testSetNetworkSpecifierWithCellularAndSatelliteMultiTransportNc() {
+ final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+ NetworkCapabilities nc = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addTransportType(TRANSPORT_SATELLITE)
+ .setNetworkSpecifier(specifier)
+ .build();
+ // Adding a specifier did not crash with 2 transports if it is cellular + satellite
+ assertEquals(specifier, nc.getNetworkSpecifier());
+ }
+
+ @Test
+ public void testSetNetworkSpecifierWithWifiAndSatelliteMultiTransportNc() {
+ final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+ NetworkCapabilities.Builder nc1 = new NetworkCapabilities.Builder();
+ nc1.addTransportType(TRANSPORT_SATELLITE).addTransportType(TRANSPORT_WIFI);
+ // Adding multiple transports specifier to crash, apart from cellular + satellite
+ // combination
+ assertThrows("Cannot set NetworkSpecifier on a NetworkCapability with multiple transports!",
+ IllegalStateException.class,
+ () -> nc1.build().setNetworkSpecifier(specifier));
+ assertThrows("Cannot set NetworkSpecifier on a NetworkCapability with multiple transports!",
+ IllegalStateException.class,
+ () -> nc1.setNetworkSpecifier(specifier));
+ }
+
+ @Test
+ public void testSetNetworkSpecifierOnTestWithCellularAndSatelliteMultiTransportNc() {
+ final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+ NetworkCapabilities nc = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_TEST)
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addTransportType(TRANSPORT_SATELLITE)
+ .setNetworkSpecifier(specifier)
+ .build();
+ // Adding a specifier did not crash with 3 transports , TEST + CELLULAR + SATELLITE and if
+ // one is test
+ assertEquals(specifier, nc.getNetworkSpecifier());
+ }
+
+ @Test
public void testSetNetworkSpecifierOnTestMultiTransportNc() {
final NetworkSpecifier specifier = CompatUtil.makeEthernetNetworkSpecifier("eth0");
NetworkCapabilities nc = new NetworkCapabilities.Builder()
diff --git a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
index 79c4980..8e89037 100644
--- a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
@@ -119,6 +119,7 @@
fullInfo.setSubtypes(Set.of("_thread", "_matter"));
fullInfo.setPort(4242);
fullInfo.setHostAddresses(List.of(IPV4_ADDRESS));
+ fullInfo.setHostname("home");
fullInfo.setNetwork(new Network(123));
fullInfo.setInterfaceIndex(456);
checkParcelable(fullInfo);
@@ -134,6 +135,7 @@
attributedInfo.setServiceType("_kitten._tcp");
attributedInfo.setPort(4242);
attributedInfo.setHostAddresses(List.of(IPV6_ADDRESS, IPV4_ADDRESS));
+ attributedInfo.setHostname("home");
attributedInfo.setAttribute("color", "pink");
attributedInfo.setAttribute("sound", (new String("にゃあ")).getBytes("UTF-8"));
attributedInfo.setAttribute("adorable", (String) null);
@@ -169,6 +171,7 @@
assertEquals(original.getServiceName(), result.getServiceName());
assertEquals(original.getServiceType(), result.getServiceType());
assertEquals(original.getHost(), result.getHost());
+ assertEquals(original.getHostname(), result.getHostname());
assertTrue(original.getPort() == result.getPort());
assertEquals(original.getNetwork(), result.getNetwork());
assertEquals(original.getInterfaceIndex(), result.getInterfaceIndex());
diff --git a/tests/cts/OWNERS b/tests/cts/OWNERS
index cb4ca59..acf506d 100644
--- a/tests/cts/OWNERS
+++ b/tests/cts/OWNERS
@@ -9,4 +9,6 @@
# For incremental changes on EthernetManagerTest to increase coverage for existing behavior and for
# testing bug fixes.
per-file net/src/android/net/cts/EthernetManagerTest.kt = prohr@google.com #{LAST_RESORT_SUGGESTION}
+# Temporary ownership to develop APF CTS tests.
+per-file net/src/android/net/cts/ApfIntegrationTest.kt = prohr@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index 923f8e2..92e7cfb 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -18,11 +18,8 @@
// downstream branches. The CtsHostsideNetworkTestsAppNext target will not exist in
// some downstream branches, but it should exist in aosp and some downstream branches.
-
-
-
-
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -30,7 +27,10 @@
name: "CtsHostsideNetworkTests",
defaults: ["cts_defaults"],
// Only compile source java files in this apk.
- srcs: ["src/**/*.java"],
+ srcs: [
+ "src/**/*.java",
+ ":ArgumentConstants",
+ ],
libs: [
"net-tests-utils-host-device-common",
"cts-tradefed",
@@ -45,7 +45,7 @@
"general-tests",
"mcts-tethering",
"mts-tethering",
- "sts"
+ "sts",
],
data: [
":CtsHostsideNetworkTestsApp",
diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp
index 2751f6f..33761dc 100644
--- a/tests/cts/hostside/aidl/Android.bp
+++ b/tests/cts/hostside/aidl/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -20,9 +21,7 @@
name: "CtsHostsideNetworkTestsAidl",
sdk_version: "current",
srcs: [
- "com/android/cts/net/hostside/IMyService.aidl",
- "com/android/cts/net/hostside/INetworkCallback.aidl",
- "com/android/cts/net/hostside/INetworkStateObserver.aidl",
- "com/android/cts/net/hostside/IRemoteSocketFactory.aidl",
+ "com/android/cts/net/hostside/*.aidl",
+ "com/android/cts/net/hostside/*.java",
],
}
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
index e7b2815..906024b 100644
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
@@ -19,11 +19,12 @@
import android.app.job.JobInfo;
import com.android.cts.net.hostside.INetworkCallback;
+import com.android.cts.net.hostside.NetworkCheckResult;
interface IMyService {
void registerBroadcastReceiver();
int getCounters(String receiverName, String action);
- String checkNetworkStatus();
+ NetworkCheckResult checkNetworkStatus(String customUrl);
String getRestrictBackgroundStatus();
void sendNotification(int notificationId, String notificationType);
void registerNetworkCallback(in NetworkRequest request, in INetworkCallback cb);
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl
index 19198c5..8ef4659 100644
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl
@@ -16,8 +16,12 @@
package com.android.cts.net.hostside;
+import android.net.NetworkInfo;
+
+import com.android.cts.net.hostside.NetworkCheckResult;
+
interface INetworkStateObserver {
- void onNetworkStateChecked(int resultCode, String resultData);
+ void onNetworkStateChecked(int resultCode, in NetworkCheckResult networkCheckResult);
const int RESULT_SUCCESS_NETWORK_STATE_CHECKED = 0;
const int RESULT_ERROR_UNEXPECTED_PROC_STATE = 1;
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/NetworkCheckResult.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/NetworkCheckResult.aidl
new file mode 100644
index 0000000..cdd6b70
--- /dev/null
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/NetworkCheckResult.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.hostside;
+
+import android.net.NetworkInfo;
+
+@JavaDerive(toString=true)
+parcelable NetworkCheckResult {
+ boolean connected;
+ String details;
+ NetworkInfo networkInfo;
+}
\ No newline at end of file
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 470bb17..cf4afa9 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -35,7 +36,10 @@
"android.test.runner",
"android.test.base",
],
- srcs: ["src/**/*.java"],
+ srcs: [
+ "src/**/*.java",
+ ":ArgumentConstants",
+ ],
// Tag this module as a cts test artifact
test_suites: [
"general-tests",
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/AbstractDefaultRestrictionsTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDefaultRestrictionsTest.java
new file mode 100644
index 0000000..8a3e790
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractDefaultRestrictionsTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.hostside;
+
+import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.os.SystemClock;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Base class for default, always-on network restrictions.
+ */
+abstract class AbstractDefaultRestrictionsTest extends AbstractRestrictBackgroundNetworkTestCase {
+
+ @Before
+ public final void setUp() throws Exception {
+ super.setUp();
+
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+
+ registerBroadcastReceiver();
+ assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
+ }
+
+ @After
+ public final void tearDown() throws Exception {
+ super.tearDown();
+
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ removePowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ }
+
+ @Test
+ public void testFgsNetworkAccess() throws Exception {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkAccess(false, null);
+
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_FOREGROUND_SERVICE);
+ }
+
+ @Test
+ public void testActivityNetworkAccess() throws Exception {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkAccess(false, null);
+
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+ }
+
+ @Test
+ public void testBackgroundNetworkAccess_inFullAllowlist() throws Exception {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkAccess(false, null);
+
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ assertNetworkAccess(true, null);
+ }
+
+ @Test
+ public void testBackgroundNetworkAccess_inExceptIdleAllowlist() throws Exception {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkAccess(false, null);
+
+ addPowerSaveModeExceptIdleWhitelist(TEST_APP2_PKG);
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ assertNetworkAccess(true, null);
+ }
+}
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..4437986 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,10 +16,15 @@
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;
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_CONNECTION_CHECK_CUSTOM_URL;
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.executeShellCommand;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.forceRunJob;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getConnectivityManager;
@@ -38,7 +43,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;
@@ -48,6 +52,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
import android.net.NetworkInfo.DetailedState;
import android.net.NetworkInfo.State;
import android.net.NetworkRequest;
@@ -63,19 +68,23 @@
import android.util.Pair;
import androidx.annotation.Nullable;
+import androidx.test.platform.app.InstrumentationRegistry;
import com.android.compatibility.common.util.AmUtils;
import com.android.compatibility.common.util.BatteryUtils;
import com.android.compatibility.common.util.DeviceConfigStateHelper;
+import com.android.compatibility.common.util.ThrowingRunnable;
+import com.android.modules.utils.build.SdkLevel;
import org.junit.Rule;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;
-import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Predicate;
/**
* Superclass for tests related to background network restrictions.
@@ -86,6 +95,8 @@
protected static final String TEST_PKG = "com.android.cts.net.hostside";
protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
+ // TODO(b/321797685): Configure it via device-config once it is available.
+ protected static final long PROCESS_STATE_TRANSITION_DELAY_MS = TimeUnit.SECONDS.toMillis(5);
private static final String TEST_APP2_ACTIVITY_CLASS = TEST_APP2_PKG + ".MyActivity";
private static final String TEST_APP2_SERVICE_CLASS = TEST_APP2_PKG + ".MyForegroundService";
@@ -93,7 +104,6 @@
private static final ComponentName TEST_JOB_COMPONENT = new ComponentName(
TEST_APP2_PKG, TEST_APP2_JOB_SERVICE_CLASS);
-
private static final int TEST_JOB_ID = 7357437;
private static final int SLEEP_TIME_SEC = 1;
@@ -126,10 +136,9 @@
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";
+ private static final String KEY_CUSTOM_URL = TEST_PKG + ".custom_url";
private static final String EMPTY_STRING = "";
@@ -150,8 +159,6 @@
private static final IntentFilter BATTERY_CHANGED_FILTER =
new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
- private static final String APP_NOT_FOREGROUND_ERROR = "app_not_fg";
-
protected static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 20_000; // 20 sec
private static final long BROADCAST_TIMEOUT_MS = 5_000;
@@ -161,6 +168,7 @@
protected ConnectivityManager mCm;
protected int mUid;
private int mMyUid;
+ private @Nullable String mCustomUrl;
private MyServiceClient mServiceClient;
private DeviceConfigStateHelper mDeviceIdleDeviceConfigStateHelper;
private PowerManager mPowerManager;
@@ -171,9 +179,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();
@@ -182,7 +187,21 @@
mUid = getUid(TEST_APP2_PKG);
mMyUid = getUid(mContext.getPackageName());
mServiceClient = new MyServiceClient(mContext);
- mServiceClient.bind();
+
+ final Bundle args = InstrumentationRegistry.getArguments();
+ mCustomUrl = args.getString(ARG_CONNECTION_CHECK_CUSTOM_URL);
+ if (mCustomUrl != null) {
+ Log.d(TAG, "Using custom URL " + mCustomUrl + " for network checks");
+ }
+
+ final int bindPriorityFlags;
+ if (Boolean.valueOf(args.getString(ARG_WAIVE_BIND_PRIORITY, "false"))) {
+ bindPriorityFlags = Context.BIND_WAIVE_PRIORITY;
+ } else {
+ bindPriorityFlags = Context.BIND_NOT_FOREGROUND;
+ }
+ mServiceClient.bind(bindPriorityFlags);
+
mPowerManager = mContext.getSystemService(PowerManager.class);
executeShellCommand("cmd netpolicy start-watching " + mUid);
// Some of the test cases assume that Data saver mode is initially disabled, which might not
@@ -206,6 +225,22 @@
if (null != lock && lock.isHeld()) lock.release();
}
+ /**
+ * Check if the feature blocking network for top_sleeping and lower priority proc-states is
+ * enabled. This is a manual check because the feature flag infrastructure may not be available
+ * in all the branches that will get this code.
+ * TODO: b/322115994 - Use @RequiresFlagsEnabled with
+ * Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE once the tests are moved to cts.
+ */
+ protected boolean isNetworkBlockedForTopSleepingAndAbove() {
+ if (!SdkLevel.isAtLeastV()) {
+ return false;
+ }
+ final String output = executeShellCommand("device_config get backstage_power"
+ + " com.android.server.net.network_blocked_for_top_sleeping_and_above");
+ return Boolean.parseBoolean(output);
+ }
+
protected int getUid(String packageName) throws Exception {
return mContext.getPackageManager().getPackageUid(packageName, 0);
}
@@ -284,44 +319,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 +366,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);
}
/**
@@ -510,25 +511,23 @@
*/
private String checkNetworkAccess(boolean expectAvailable,
@Nullable final String expectedUnavailableError) throws Exception {
- final String resultData = mServiceClient.checkNetworkStatus();
- return checkForAvailabilityInResultData(resultData, expectAvailable,
+ final NetworkCheckResult checkResult = mServiceClient.checkNetworkStatus(mCustomUrl);
+ return checkForAvailabilityInNetworkCheckResult(checkResult, expectAvailable,
expectedUnavailableError);
}
- private String checkForAvailabilityInResultData(String resultData, boolean expectAvailable,
- @Nullable final String expectedUnavailableError) {
- if (resultData == null) {
- assertNotNull("Network status from app2 is null", resultData);
- }
- // Network status format is described on MyBroadcastReceiver.checkNetworkStatus()
- final String[] parts = resultData.split(NETWORK_STATUS_SEPARATOR);
- assertEquals("Wrong network status: " + resultData, 5, parts.length);
- final State state = parts[0].equals("null") ? null : State.valueOf(parts[0]);
- final DetailedState detailedState = parts[1].equals("null")
- ? null : DetailedState.valueOf(parts[1]);
- final boolean connected = Boolean.valueOf(parts[2]);
- final String connectionCheckDetails = parts[3];
- final String networkInfo = parts[4];
+ private String checkForAvailabilityInNetworkCheckResult(NetworkCheckResult networkCheckResult,
+ boolean expectAvailable, @Nullable final String expectedUnavailableError) {
+ assertNotNull("NetworkCheckResult from app2 is null", networkCheckResult);
+
+ final NetworkInfo networkInfo = networkCheckResult.networkInfo;
+ assertNotNull("NetworkInfo from app2 is null", networkInfo);
+
+ final State state = networkInfo.getState();
+ final DetailedState detailedState = networkInfo.getDetailedState();
+
+ final boolean connected = networkCheckResult.connected;
+ final String connectionCheckDetails = networkCheckResult.details;
final StringBuilder errors = new StringBuilder();
final State expectedState;
@@ -934,33 +933,36 @@
if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
startForegroundService();
assertForegroundServiceNetworkAccess();
- return;
} else if (type == TYPE_COMPONENT_ACTIVTIY) {
turnScreenOn();
final CountDownLatch latch = new CountDownLatch(1);
final Intent launchIntent = getIntentForComponent(type);
final Bundle extras = new Bundle();
- final ArrayList<Pair<Integer, String>> result = new ArrayList<>(1);
+ final AtomicReference<Pair<Integer, NetworkCheckResult>> result =
+ new AtomicReference<>();
extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
+ extras.putString(KEY_CUSTOM_URL, mCustomUrl);
launchIntent.putExtras(extras);
mContext.startActivity(launchIntent);
if (latch.await(ACTIVITY_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
- final int resultCode = result.get(0).first;
- final String resultData = result.get(0).second;
+ final int resultCode = result.get().first;
+ final NetworkCheckResult networkCheckResult = result.get().second;
if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
- final String error = checkForAvailabilityInResultData(
- resultData, expectAvailable, null /* expectedUnavailableError */);
+ final String error = checkForAvailabilityInNetworkCheckResult(
+ networkCheckResult, expectAvailable,
+ null /* expectedUnavailableError */);
if (error != null) {
fail("Network is not available for activity in app2 (" + mUid + "): "
+ error);
}
} else if (resultCode == INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE) {
- Log.d(TAG, resultData);
+ Log.d(TAG, networkCheckResult.details);
// App didn't come to foreground when the activity is started, so try again.
- assertForegroundNetworkAccess();
+ assertTopNetworkAccess(true);
} else {
- fail("Unexpected resultCode=" + resultCode + "; received=[" + resultData + "]");
+ fail("Unexpected resultCode=" + resultCode
+ + "; networkCheckResult=[" + networkCheckResult + "]");
}
} else {
fail("Timed out waiting for network availability status from app2's activity ("
@@ -968,10 +970,12 @@
}
} else if (type == TYPE_EXPEDITED_JOB) {
final Bundle extras = new Bundle();
- final ArrayList<Pair<Integer, String>> result = new ArrayList<>(1);
+ final AtomicReference<Pair<Integer, NetworkCheckResult>> result =
+ new AtomicReference<>();
final CountDownLatch latch = new CountDownLatch(1);
extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
+ extras.putString(KEY_CUSTOM_URL, mCustomUrl);
final JobInfo jobInfo = new JobInfo.Builder(TEST_JOB_ID, TEST_JOB_COMPONENT)
.setExpedited(true)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
@@ -981,11 +985,12 @@
RESULT_SUCCESS, mServiceClient.scheduleJob(jobInfo));
forceRunJob(TEST_APP2_PKG, TEST_JOB_ID);
if (latch.await(JOB_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
- final int resultCode = result.get(0).first;
- final String resultData = result.get(0).second;
+ final int resultCode = result.get().first;
+ final NetworkCheckResult networkCheckResult = result.get().second;
if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
- final String error = checkForAvailabilityInResultData(
- resultData, expectAvailable, null /* expectedUnavailableError */);
+ final String error = checkForAvailabilityInNetworkCheckResult(
+ networkCheckResult, expectAvailable,
+ null /* expectedUnavailableError */);
if (error != null) {
Log.d(TAG, "Network state is unexpected, checking again. " + error);
// Right now we could end up in an unexpected state if expedited job
@@ -993,7 +998,8 @@
assertNetworkAccess(expectAvailable, false /* needScreenOn */);
}
} else {
- fail("Unexpected resultCode=" + resultCode + "; received=[" + resultData + "]");
+ fail("Unexpected resultCode=" + resultCode
+ + "; networkCheckResult=[" + networkCheckResult + "]");
}
} else {
fail("Timed out waiting for network availability status from app2's expedited job ("
@@ -1036,11 +1042,12 @@
}
private Binder getNewNetworkStateObserver(final CountDownLatch latch,
- final ArrayList<Pair<Integer, String>> result) {
+ final AtomicReference<Pair<Integer, NetworkCheckResult>> result) {
return new INetworkStateObserver.Stub() {
@Override
- public void onNetworkStateChecked(int resultCode, String resultData) {
- result.add(Pair.create(resultCode, resultData));
+ public void onNetworkStateChecked(int resultCode,
+ NetworkCheckResult networkCheckResult) {
+ result.set(Pair.create(resultCode, networkCheckResult));
latch.countDown();
}
};
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..3e22a23 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,9 @@
package com.android.cts.net.hostside;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
+
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getUiDevice;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
@@ -26,8 +29,13 @@
import static com.android.cts.net.hostside.Property.METERED_NETWORK;
import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+import static org.junit.Assume.assumeTrue;
+
+import android.os.SystemClock;
import android.util.Log;
+import com.android.compatibility.common.util.ThrowingRunnable;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -61,23 +69,26 @@
@RequiredProperties({BATTERY_SAVER_MODE})
public void testStartActivity_batterySaver() throws Exception {
setBatterySaverMode(true);
- assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver");
+ assertNetworkAccess(false, null);
+ assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver", null);
}
@Test
@RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
public void testStartActivity_dataSaver() throws Exception {
setRestrictBackground(true);
- assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver");
+ assertNetworkAccess(false, null);
+ assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver", null);
}
@Test
@RequiredProperties({DOZE_MODE})
public void testStartActivity_doze() throws Exception {
setDozeMode(true);
+ assertNetworkAccess(false, null);
// TODO (235284115): We need to turn on Doze every time before starting
// the activity.
- assertLaunchedActivityHasNetworkAccess("testStartActivity_doze");
+ assertLaunchedActivityHasNetworkAccess("testStartActivity_doze", null);
}
@Test
@@ -85,17 +96,32 @@
public void testStartActivity_appStandby() throws Exception {
turnBatteryOn();
setAppIdle(true);
+ assertNetworkAccess(false, null);
// TODO (235284115): We need to put the app into app standby mode every
// time before starting the activity.
- assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby");
+ assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby", null);
}
- private void assertLaunchedActivityHasNetworkAccess(String testName) throws Exception {
+ @Test
+ public void testStartActivity_default() throws Exception {
+ assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
+ assertLaunchedActivityHasNetworkAccess("testStartActivity_default", () -> {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkAccess(false, null);
+ });
+ }
+
+ private void assertLaunchedActivityHasNetworkAccess(String testName,
+ ThrowingRunnable onBeginIteration) throws Exception {
for (int i = 0; i < TEST_ITERATION_COUNT; ++i) {
+ if (onBeginIteration != null) {
+ onBeginIteration.run();
+ }
Log.i(TAG, testName + " start #" + i);
launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
getUiDevice().pressHome();
- 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/DefaultRestrictionsMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsMeteredTest.java
new file mode 100644
index 0000000..f3a1026
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+
+@RequiredProperties({METERED_NETWORK})
+public class DefaultRestrictionsMeteredTest extends AbstractDefaultRestrictionsTest {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsNonMeteredTest.java
new file mode 100644
index 0000000..5651dd0
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DefaultRestrictionsNonMeteredTest.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.hostside;
+
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class DefaultRestrictionsNonMeteredTest extends AbstractDefaultRestrictionsTest {
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
index 93cc911..494192f 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
@@ -34,26 +34,30 @@
private Context mContext;
private ServiceConnection mServiceConnection;
- private IMyService mService;
+ private volatile IMyService mService;
+ private final ConditionVariable mServiceCondition = new ConditionVariable();
public MyServiceClient(Context context) {
mContext = context;
}
- public void bind() {
+ /**
+ * Binds to a service in the test app to communicate state.
+ * @param bindPriorityFlags Flags to influence the process-state of the bound app.
+ */
+ public void bind(int bindPriorityFlags) {
if (mService != null) {
throw new IllegalStateException("Already bound");
}
-
- final ConditionVariable cv = new ConditionVariable();
mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = IMyService.Stub.asInterface(service);
- cv.open();
+ mServiceCondition.open();
}
@Override
public void onServiceDisconnected(ComponentName name) {
+ mServiceCondition.close();
mService = null;
}
};
@@ -63,12 +67,8 @@
// Needs to use BIND_NOT_FOREGROUND so app2 does not run in
// the same process state as app
mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE
- | Context.BIND_NOT_FOREGROUND);
- cv.block(TIMEOUT_MS);
- if (mService == null) {
- throw new IllegalStateException(
- "Could not bind to MyService service after " + TIMEOUT_MS + "ms");
- }
+ | bindPriorityFlags);
+ ensureServiceConnection();
}
public void unbind() {
@@ -77,37 +77,57 @@
}
}
+ private void ensureServiceConnection() {
+ if (mService != null) {
+ return;
+ }
+ mServiceCondition.block(TIMEOUT_MS);
+ if (mService == null) {
+ throw new IllegalStateException(
+ "Could not bind to MyService service after " + TIMEOUT_MS + "ms");
+ }
+ }
+
public void registerBroadcastReceiver() throws RemoteException {
+ ensureServiceConnection();
mService.registerBroadcastReceiver();
}
public int getCounters(String receiverName, String action) throws RemoteException {
+ ensureServiceConnection();
return mService.getCounters(receiverName, action);
}
- public String checkNetworkStatus() throws RemoteException {
- return mService.checkNetworkStatus();
+ /** Retrieves the network state as observed from the bound test app */
+ public NetworkCheckResult checkNetworkStatus(String address) throws RemoteException {
+ ensureServiceConnection();
+ return mService.checkNetworkStatus(address);
}
public String getRestrictBackgroundStatus() throws RemoteException {
+ ensureServiceConnection();
return mService.getRestrictBackgroundStatus();
}
public void sendNotification(int notificationId, String notificationType)
throws RemoteException {
+ ensureServiceConnection();
mService.sendNotification(notificationId, notificationType);
}
public void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb)
throws RemoteException {
+ ensureServiceConnection();
mService.registerNetworkCallback(request, cb);
}
public void unregisterNetworkCallback() throws RemoteException {
+ ensureServiceConnection();
mService.unregisterNetworkCallback();
}
public int scheduleJob(JobInfo jobInfo) throws RemoteException {
+ ensureServiceConnection();
return mService.scheduleJob(jobInfo);
}
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
index ab956bf..5552b8f 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,8 @@
package com.android.cts.net.hostside;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
@@ -33,6 +35,7 @@
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.cts.util.CtsNetUtils;
+import android.os.SystemClock;
import android.util.Log;
import com.android.modules.utils.build.SdkLevel;
@@ -42,6 +45,7 @@
import org.junit.Rule;
import org.junit.Test;
+import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@@ -144,12 +148,22 @@
public Network expectAvailableCallbackAndGetNetwork() {
final CallbackInfo cb = nextCallback(TEST_CONNECT_TIMEOUT_MS);
if (cb.state != CallbackState.AVAILABLE) {
- fail("Network is not available. Instead obtained the following callback :"
- + cb);
+ fail("Network is not available. Instead obtained the following callback :" + cb);
}
return cb.network;
}
+ public void drainAndWaitForIdle() {
+ try {
+ do {
+ mCallbacks.drainTo(new ArrayList<>());
+ } while (mCallbacks.poll(TEST_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS) != null);
+ } catch (InterruptedException ie) {
+ Log.e(TAG, "Interrupted while draining callback queue", ie);
+ Thread.currentThread().interrupt();
+ }
+ }
+
public void expectBlockedStatusCallback(Network expectedNetwork, boolean expectBlocked) {
expectCallback(CallbackState.BLOCKED_STATUS, expectedNetwork, expectBlocked);
}
@@ -224,7 +238,7 @@
// Check that the network is metered.
mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
false /* hasCapability */, NET_CAPABILITY_NOT_METERED);
- mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+ mTestNetworkCallback.drainAndWaitForIdle();
// Before Android T, DNS queries over private DNS should be but are not restricted by Power
// Saver or Data Saver. The issue is fixed in mainline update and apps can no longer request
@@ -313,7 +327,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);
}
@@ -337,7 +352,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);
}
@@ -354,6 +370,58 @@
}
}
+ @Test
+ public void testOnBlockedStatusChanged_default() throws Exception {
+ assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
+
+ try {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ assertNetworkAccess(false, null);
+ assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
+
+ launchActivity();
+ assertTopState();
+ assertNetworkAccess(true, null);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+ assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
+
+ finishActivity();
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkAccess(false, null);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+ assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
+
+ } finally {
+ mMeterednessConfiguration.resetNetworkMeteredness();
+ }
+
+ // Set to non-metered network
+ mMeterednessConfiguration.configureNetworkMeteredness(false);
+ mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
+ true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
+ try {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ assertNetworkAccess(false, null);
+ assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
+
+ launchActivity();
+ assertTopState();
+ assertNetworkAccess(true, null);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+ assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
+
+ finishActivity();
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkAccess(false, null);
+ mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+ assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
+ } finally {
+ mMeterednessConfiguration.resetNetworkMeteredness();
+ }
+ }
+
// TODO: 1. test against VPN lockdown.
// 2. test against multiple networks.
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
index a0d88c9..968e270 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,8 @@
package com.android.cts.net.hostside;
+import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
import static android.os.Process.SYSTEM_UID;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.assertIsUidRestrictedOnMeteredNetworks;
@@ -27,6 +29,9 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.os.SystemClock;
import org.junit.After;
import org.junit.Before;
@@ -137,13 +142,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 +224,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.
@@ -237,4 +242,33 @@
assertIsUidRestrictedOnMeteredNetworks(mUid, false /* expectedResult */);
}
}
+
+ @Test
+ public void testIsUidNetworkingBlocked_whenInBackground() throws Exception {
+ assumeTrue("Feature not enabled", isNetworkBlockedForTopSleepingAndAbove());
+
+ try {
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkingBlockedStatusForUid(mUid, METERED, true /* expectedResult */);
+ assertTrue(isUidNetworkingBlocked(mUid, NON_METERED));
+
+ launchActivity();
+ assertTopState();
+ assertNetworkingBlockedStatusForUid(mUid, METERED, false /* expectedResult */);
+ assertFalse(isUidNetworkingBlocked(mUid, NON_METERED));
+
+ finishActivity();
+ assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
+ SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+ assertNetworkingBlockedStatusForUid(mUid, METERED, true /* expectedResult */);
+ assertTrue(isUidNetworkingBlocked(mUid, NON_METERED));
+
+ addPowerSaveModeWhitelist(TEST_APP2_PKG);
+ assertNetworkingBlockedStatusForUid(mUid, METERED, false /* expectedResult */);
+ assertFalse(isUidNetworkingBlocked(mUid, NON_METERED));
+ } finally {
+ removePowerSaveModeWhitelist(TEST_APP2_PKG);
+ }
+ }
}
diff --git a/tests/cts/hostside/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/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index 454940f..0f86d78 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -23,6 +23,7 @@
import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
import static android.content.pm.PackageManager.FEATURE_WIFI;
import static android.net.ConnectivityManager.TYPE_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.os.Process.INVALID_UID;
import static android.system.OsConstants.AF_INET;
@@ -72,6 +73,7 @@
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
+import android.net.InetAddresses;
import android.net.IpSecManager;
import android.net.LinkAddress;
import android.net.LinkProperties;
@@ -91,6 +93,7 @@
import android.net.cts.util.CtsNetUtils;
import android.net.util.KeepaliveUtils;
import android.net.wifi.WifiManager;
+import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
@@ -120,6 +123,7 @@
import com.android.net.module.util.ArrayTrackRecord;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.PacketBuilder;
+import com.android.testutils.AutoReleaseNetworkCallbackRule;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.RecorderCallback;
@@ -230,9 +234,13 @@
// The registered callbacks.
private List<NetworkCallback> mRegisteredCallbacks = new ArrayList<>();
- @Rule
+ @Rule(order = 1)
public final DevSdkIgnoreRule mDevSdkIgnoreRule = new DevSdkIgnoreRule();
+ @Rule(order = 2)
+ public final AutoReleaseNetworkCallbackRule
+ mNetworkCallbackRule = new AutoReleaseNetworkCallbackRule();
+
private boolean supportedHardware() {
final PackageManager pm = getInstrumentation().getContext().getPackageManager();
return !pm.hasSystemFeature("android.hardware.type.watch");
@@ -249,6 +257,7 @@
@Before
public void setUp() throws Exception {
+ assumeTrue(supportedHardware());
mNetwork = null;
mTestContext = getInstrumentation().getContext();
mTargetContext = getInstrumentation().getTargetContext();
@@ -269,7 +278,6 @@
public void tearDown() throws Exception {
restorePrivateDnsSetting();
mRemoteSocketFactoryClient.unbind();
- mCtsNetUtils.tearDown();
Log.i(TAG, "Stopping VPN");
stopVpn();
unregisterRegisteredCallbacks();
@@ -879,7 +887,6 @@
@Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
public void testChangeUnderlyingNetworks() throws Exception {
- assumeTrue(supportedHardware());
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
final TestableNetworkCallback callback = new TestableNetworkCallback();
@@ -887,9 +894,7 @@
testAndCleanup(() -> {
// Ensure both of wifi and mobile data are connected.
final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
- assertTrue("Wifi is not connected", (wifiNetwork != null));
- final Network cellNetwork = mCtsNetUtils.connectToCell();
- assertTrue("Mobile data is not connected", (cellNetwork != null));
+ final Network cellNetwork = mNetworkCallbackRule.requestCell();
// Store current default network.
final Network defaultNetwork = mCM.getActiveNetwork();
// Start VPN and set empty array as its underlying networks.
@@ -938,7 +943,6 @@
@Test
public void testDefault() throws Exception {
- assumeTrue(supportedHardware());
if (!SdkLevel.isAtLeastS() && (
SystemProperties.getInt("persist.adb.tcp.port", -1) > -1
|| SystemProperties.getInt("service.adb.tcp.port", -1) > -1)) {
@@ -1017,8 +1021,8 @@
// This needs to be done before testing private DNS because checkStrictModePrivateDns
// will set the private DNS server to a nonexistent name, which will cause validation to
// fail and could cause the default network to switch (e.g., from wifi to cellular).
- systemDefaultCallback.assertNoCallback();
- otherUidCallback.assertNoCallback();
+ assertNoCallbackExceptCapOrLpChange(systemDefaultCallback);
+ assertNoCallbackExceptCapOrLpChange(otherUidCallback);
}
checkStrictModePrivateDns();
@@ -1026,10 +1030,13 @@
receiver.unregisterQuietly();
}
+ private void assertNoCallbackExceptCapOrLpChange(TestableNetworkCallback callback) {
+ callback.assertNoCallback(c -> !(c instanceof CallbackEntry.CapabilitiesChanged
+ || c instanceof CallbackEntry.LinkPropertiesChanged));
+ }
+
@Test
public void testAppAllowed() throws Exception {
- assumeTrue(supportedHardware());
-
FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
// Shell app must not be put in here or it would kill the ADB-over-network use case
@@ -1137,8 +1144,6 @@
}
private void doTestAutomaticOnOffKeepaliveMode(final boolean closeSocket) throws Exception {
- assumeTrue(supportedHardware());
-
// Get default network first before starting VPN
final Network defaultNetwork = mCM.getActiveNetwork();
final TestableNetworkCallback cb = new TestableNetworkCallback();
@@ -1226,8 +1231,6 @@
@Test
public void testAppDisallowed() throws Exception {
- assumeTrue(supportedHardware());
-
FileDescriptor localFd = openSocketFd(TEST_HOST, 80, TIMEOUT_MS);
FileDescriptor remoteFd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
@@ -1260,8 +1263,6 @@
@Test
public void testSocketClosed() throws Exception {
- assumeTrue(supportedHardware());
-
final FileDescriptor localFd = openSocketFd(TEST_HOST, 80, TIMEOUT_MS);
final List<FileDescriptor> remoteFds = new ArrayList<>();
@@ -1285,7 +1286,6 @@
@Test
public void testExcludedRoutes() throws Exception {
- assumeTrue(supportedHardware());
assumeTrue(SdkLevel.isAtLeastT());
// Shell app must not be put in here or it would kill the ADB-over-network use case
@@ -1306,8 +1306,6 @@
@Test
public void testIncludedRoutes() throws Exception {
- assumeTrue(supportedHardware());
-
// Shell app must not be put in here or it would kill the ADB-over-network use case
String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
@@ -1325,7 +1323,6 @@
@Test
public void testInterleavedRoutes() throws Exception {
- assumeTrue(supportedHardware());
assumeTrue(SdkLevel.isAtLeastT());
// Shell app must not be put in here or it would kill the ADB-over-network use case
@@ -1353,8 +1350,6 @@
@Test
public void testGetConnectionOwnerUidSecurity() throws Exception {
- assumeTrue(supportedHardware());
-
DatagramSocket s;
InetAddress address = InetAddress.getByName("localhost");
s = new DatagramSocket();
@@ -1375,7 +1370,6 @@
@Test
public void testSetProxy() throws Exception {
- assumeTrue(supportedHardware());
ProxyInfo initialProxy = mCM.getDefaultProxy();
// Receiver for the proxy change broadcast.
BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
@@ -1415,7 +1409,6 @@
@Test
public void testSetProxyDisallowedApps() throws Exception {
- assumeTrue(supportedHardware());
ProxyInfo initialProxy = mCM.getDefaultProxy();
String disallowedApps = mPackageName;
@@ -1441,7 +1434,6 @@
@Test
public void testNoProxy() throws Exception {
- assumeTrue(supportedHardware());
ProxyInfo initialProxy = mCM.getDefaultProxy();
BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
proxyBroadcastReceiver.register();
@@ -1476,7 +1468,6 @@
@Test
public void testBindToNetworkWithProxy() throws Exception {
- assumeTrue(supportedHardware());
String allowedApps = mPackageName;
Network initialNetwork = mCM.getActiveNetwork();
ProxyInfo initialProxy = mCM.getDefaultProxy();
@@ -1501,9 +1492,6 @@
@Test
public void testVpnMeterednessWithNoUnderlyingNetwork() throws Exception {
- if (!supportedHardware()) {
- return;
- }
// VPN is not routing any traffic i.e. its underlying networks is an empty array.
ArrayList<Network> underlyingNetworks = new ArrayList<>();
String allowedApps = mPackageName;
@@ -1533,9 +1521,6 @@
@Test
public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception {
- if (!supportedHardware()) {
- return;
- }
Network underlyingNetwork = mCM.getActiveNetwork();
if (underlyingNetwork == null) {
Log.i(TAG, "testVpnMeterednessWithNullUnderlyingNetwork cannot execute"
@@ -1562,9 +1547,6 @@
@Test
public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception {
- if (!supportedHardware()) {
- return;
- }
Network underlyingNetwork = mCM.getActiveNetwork();
if (underlyingNetwork == null) {
Log.i(TAG, "testVpnMeterednessWithNonNullUnderlyingNetwork cannot execute"
@@ -1604,9 +1586,6 @@
@Test
public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception {
- if (!supportedHardware()) {
- return;
- }
Network underlyingNetwork = mCM.getActiveNetwork();
if (underlyingNetwork == null) {
Log.i(TAG, "testAlwaysMeteredVpnWithNullUnderlyingNetwork cannot execute"
@@ -1631,9 +1610,6 @@
@Test
public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception {
- if (!supportedHardware()) {
- return;
- }
Network underlyingNetwork = mCM.getActiveNetwork();
if (underlyingNetwork == null) {
Log.i(TAG, "testAlwaysMeteredVpnWithNonNullUnderlyingNetwork cannot execute"
@@ -1671,9 +1647,6 @@
@Test
public void testB141603906() throws Exception {
- if (!supportedHardware()) {
- return;
- }
final InetSocketAddress src = new InetSocketAddress(0);
final InetSocketAddress dst = new InetSocketAddress(0);
final int NUM_THREADS = 8;
@@ -1781,8 +1754,6 @@
*/
@Test
public void testDownloadWithDownloadManagerDisallowed() throws Exception {
- assumeTrue(supportedHardware());
-
// Start a VPN with DownloadManager package in disallowed list.
startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
new String[] {"192.0.2.0/24", "2001:db8::/32"},
@@ -1838,7 +1809,6 @@
@Test @IgnoreUpTo(Build.VERSION_CODES.R)
public void testBlockIncomingPackets() throws Exception {
- assumeTrue(supportedHardware());
final Network network = mCM.getActiveNetwork();
assertNotNull("Requires a working Internet connection", network);
@@ -1907,7 +1877,6 @@
@Test
public void testSetVpnDefaultForUids() throws Exception {
- assumeTrue(supportedHardware());
assumeTrue(SdkLevel.isAtLeastU());
final Network defaultNetwork = mCM.getActiveNetwork();
@@ -1953,6 +1922,81 @@
});
}
+ /**
+ * Check if packets to a VPN interface's IP arriving on a non-VPN interface are dropped or not.
+ * If the test interface has a different address from the VPN interface, packets must be dropped
+ * If the test interface has the same address as the VPN interface, packets must not be
+ * dropped
+ *
+ * @param duplicatedAddress true to bring up the test interface with the same address as the VPN
+ * interface
+ */
+ private void doTestDropPacketToVpnAddress(final boolean duplicatedAddress)
+ throws Exception {
+ final NetworkRequest request = new NetworkRequest.Builder()
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ .build();
+ final CtsNetUtils.TestNetworkCallback callback = new CtsNetUtils.TestNetworkCallback();
+ mCM.requestNetwork(request, callback);
+ final FileDescriptor srcTunFd = runWithShellPermissionIdentity(() -> {
+ final TestNetworkManager tnm = mTestContext.getSystemService(TestNetworkManager.class);
+ List<LinkAddress> linkAddresses = duplicatedAddress
+ ? List.of(new LinkAddress("192.0.2.2/24"),
+ new LinkAddress("2001:db8:1:2::ffe/64")) :
+ List.of(new LinkAddress("198.51.100.2/24"),
+ new LinkAddress("2001:db8:3:4::ffe/64"));
+ final TestNetworkInterface iface = tnm.createTunInterface(linkAddresses);
+ tnm.setupTestNetwork(iface.getInterfaceName(), new Binder());
+ return iface.getFileDescriptor().getFileDescriptor();
+ }, MANAGE_TEST_NETWORKS);
+ final Network testNetwork = callback.waitForAvailable();
+ assertNotNull(testNetwork);
+ final DatagramSocket dstSock = new DatagramSocket();
+
+ testAndCleanup(() -> {
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
+ new String[]{"0.0.0.0/0", "::/0"} /* routes */,
+ "" /* allowedApplications */, "" /* disallowedApplications */,
+ null /* proxyInfo */, null /* underlyingNetworks */,
+ false /* isAlwaysMetered */);
+
+ final FileDescriptor dstUdpFd = dstSock.getFileDescriptor$();
+ checkBlockUdp(srcTunFd, dstUdpFd,
+ InetAddresses.parseNumericAddress("192.0.2.2") /* dstAddress */,
+ InetAddresses.parseNumericAddress("192.0.2.1") /* srcAddress */,
+ duplicatedAddress ? EXPECT_PASS : EXPECT_BLOCK);
+ checkBlockUdp(srcTunFd, dstUdpFd,
+ InetAddresses.parseNumericAddress("2001:db8:1:2::ffe") /* dstAddress */,
+ InetAddresses.parseNumericAddress("2001:db8:1:2::ffa") /* srcAddress */,
+ duplicatedAddress ? EXPECT_PASS : EXPECT_BLOCK);
+
+ // Traffic on VPN should not be affected
+ checkTrafficOnVpn();
+ }, /* cleanup */ () -> {
+ Os.close(srcTunFd);
+ dstSock.close();
+ }, /* cleanup */ () -> {
+ runWithShellPermissionIdentity(() -> {
+ mTestContext.getSystemService(TestNetworkManager.class)
+ .teardownTestNetwork(testNetwork);
+ }, MANAGE_TEST_NETWORKS);
+ }, /* cleanup */ () -> {
+ mCM.unregisterNetworkCallback(callback);
+ });
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testDropPacketToVpnAddress_WithoutDuplicatedAddress() throws Exception {
+ doTestDropPacketToVpnAddress(false /* duplicatedAddress */);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testDropPacketToVpnAddress_WithDuplicatedAddress() throws Exception {
+ doTestDropPacketToVpnAddress(true /* duplicatedAddress */);
+ }
+
private ByteBuffer buildIpv4UdpPacket(final Inet4Address dstAddr, final Inet4Address srcAddr,
final short dstPort, final short srcPort, final byte[] payload) throws IOException {
@@ -1996,7 +2040,8 @@
private void checkBlockUdp(
final FileDescriptor srcTunFd,
final FileDescriptor dstUdpFd,
- final boolean ipv6,
+ final InetAddress dstAddress,
+ final InetAddress srcAddress,
final boolean expectBlock) throws Exception {
final Random random = new Random();
final byte[] sendData = new byte[100];
@@ -2004,15 +2049,15 @@
final short dstPort = (short) ((InetSocketAddress) Os.getsockname(dstUdpFd)).getPort();
ByteBuffer buf;
- if (ipv6) {
+ if (dstAddress instanceof Inet6Address) {
buf = buildIpv6UdpPacket(
- (Inet6Address) TEST_IP6_DST_ADDR.getAddress(),
- (Inet6Address) TEST_IP6_SRC_ADDR.getAddress(),
+ (Inet6Address) dstAddress,
+ (Inet6Address) srcAddress,
dstPort, TEST_SRC_PORT, sendData);
} else {
buf = buildIpv4UdpPacket(
- (Inet4Address) TEST_IP4_DST_ADDR.getAddress(),
- (Inet4Address) TEST_IP4_SRC_ADDR.getAddress(),
+ (Inet4Address) dstAddress,
+ (Inet4Address) srcAddress,
dstPort, TEST_SRC_PORT, sendData);
}
@@ -2038,8 +2083,10 @@
final FileDescriptor srcTunFd,
final FileDescriptor dstUdpFd,
final boolean expectBlock) throws Exception {
- checkBlockUdp(srcTunFd, dstUdpFd, false /* ipv6 */, expectBlock);
- checkBlockUdp(srcTunFd, dstUdpFd, true /* ipv6 */, expectBlock);
+ checkBlockUdp(srcTunFd, dstUdpFd, TEST_IP4_DST_ADDR.getAddress(),
+ TEST_IP4_SRC_ADDR.getAddress(), expectBlock);
+ checkBlockUdp(srcTunFd, dstUdpFd, TEST_IP6_DST_ADDR.getAddress(),
+ TEST_IP6_SRC_ADDR.getAddress(), expectBlock);
}
private class DetailedBlockedStatusCallback extends TestableNetworkCallback {
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index db92f5c..c526172 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
index 37dc7a0..1c45579 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
@@ -15,10 +15,16 @@
*/
package com.android.cts.net.hostside.app2;
+import static com.android.cts.net.hostside.INetworkStateObserver.RESULT_ERROR_OTHER;
+import static com.android.cts.net.hostside.INetworkStateObserver.RESULT_ERROR_UNEXPECTED_CAPABILITIES;
+import static com.android.cts.net.hostside.INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE;
+
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Process;
@@ -26,6 +32,12 @@
import android.util.Log;
import com.android.cts.net.hostside.INetworkStateObserver;
+import com.android.cts.net.hostside.NetworkCheckResult;
+
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
public final class Common {
@@ -48,6 +60,9 @@
static final String ACTION_SNOOZE_WARNING =
"com.android.server.net.action.SNOOZE_WARNING";
+ private static final String DEFAULT_TEST_URL =
+ "https://connectivitycheck.android.com/generate_204";
+
static final String NOTIFICATION_TYPE_CONTENT = "CONTENT";
static final String NOTIFICATION_TYPE_DELETE = "DELETE";
static final String NOTIFICATION_TYPE_FULL_SCREEN = "FULL_SCREEN";
@@ -59,10 +74,12 @@
static final String TEST_PKG = "com.android.cts.net.hostside";
static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks";
+ static final String KEY_CUSTOM_URL = TEST_PKG + ".custom_url";
static final int TYPE_COMPONENT_ACTIVTY = 0;
static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
static final int TYPE_COMPONENT_EXPEDITED_JOB = 2;
+ private static final int NETWORK_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(10);
static int getUid(Context context) {
final String packageName = context.getPackageName();
@@ -73,6 +90,15 @@
}
}
+ private static NetworkCheckResult createNetworkCheckResult(boolean connected, String details,
+ NetworkInfo networkInfo) {
+ final NetworkCheckResult checkResult = new NetworkCheckResult();
+ checkResult.connected = connected;
+ checkResult.details = details;
+ checkResult.networkInfo = networkInfo;
+ return checkResult;
+ }
+
private static boolean validateComponentState(Context context, int componentType,
INetworkStateObserver observer) throws RemoteException {
final ActivityManager activityManager = context.getSystemService(ActivityManager.class);
@@ -80,9 +106,9 @@
case TYPE_COMPONENT_ACTIVTY: {
final int procState = activityManager.getUidProcessState(Process.myUid());
if (procState != ActivityManager.PROCESS_STATE_TOP) {
- observer.onNetworkStateChecked(
- INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE,
- "Unexpected procstate: " + procState);
+ observer.onNetworkStateChecked(RESULT_ERROR_UNEXPECTED_PROC_STATE,
+ createNetworkCheckResult(false, "Unexpected procstate: " + procState,
+ null));
return false;
}
return true;
@@ -90,9 +116,9 @@
case TYPE_COMPONENT_FOREGROUND_SERVICE: {
final int procState = activityManager.getUidProcessState(Process.myUid());
if (procState != ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
- observer.onNetworkStateChecked(
- INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE,
- "Unexpected procstate: " + procState);
+ observer.onNetworkStateChecked(RESULT_ERROR_UNEXPECTED_PROC_STATE,
+ createNetworkCheckResult(false, "Unexpected procstate: " + procState,
+ null));
return false;
}
return true;
@@ -101,16 +127,17 @@
final int capabilities = activityManager.getUidProcessCapabilities(Process.myUid());
if ((capabilities
& ActivityManager.PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK) == 0) {
- observer.onNetworkStateChecked(
- INetworkStateObserver.RESULT_ERROR_UNEXPECTED_CAPABILITIES,
- "Unexpected capabilities: " + capabilities);
+ observer.onNetworkStateChecked(RESULT_ERROR_UNEXPECTED_CAPABILITIES,
+ createNetworkCheckResult(false,
+ "Unexpected capabilities: " + capabilities, null));
return false;
}
return true;
}
default: {
- observer.onNetworkStateChecked(INetworkStateObserver.RESULT_ERROR_OTHER,
- "Unknown component type: " + componentType);
+ observer.onNetworkStateChecked(RESULT_ERROR_OTHER,
+ createNetworkCheckResult(false, "Unknown component type: " + componentType,
+ null));
return false;
}
}
@@ -131,6 +158,7 @@
final INetworkStateObserver observer = INetworkStateObserver.Stub.asInterface(
extras.getBinder(KEY_NETWORK_STATE_OBSERVER));
if (observer != null) {
+ final String customUrl = extras.getString(KEY_CUSTOM_URL);
try {
final boolean skipValidation = extras.getBoolean(KEY_SKIP_VALIDATION_CHECKS);
if (!skipValidation && !validateComponentState(context, componentType, observer)) {
@@ -143,11 +171,64 @@
try {
observer.onNetworkStateChecked(
INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED,
- MyBroadcastReceiver.checkNetworkStatus(context));
+ checkNetworkStatus(context, customUrl));
} catch (RemoteException e) {
Log.e(TAG, "Error occurred while notifying the observer: " + e);
}
});
}
}
+
+ /**
+ * Checks whether the network is available by attempting a connection to the given address
+ * and returns a {@link NetworkCheckResult} object containing all the relevant details for
+ * debugging. Uses a default address if the given address is {@code null}.
+ *
+ * <p>
+ * The returned object has the following fields:
+ *
+ * <ul>
+ * <li>{@code connected}: whether or not the connection was successful.
+ * <li>{@code networkInfo}: the {@link NetworkInfo} describing the current active network as
+ * visible to this app.
+ * <li>{@code details}: A human readable string giving useful information about the success or
+ * failure.
+ * </ul>
+ */
+ static NetworkCheckResult checkNetworkStatus(Context context, String customUrl) {
+ final String address = (customUrl == null) ? DEFAULT_TEST_URL : customUrl;
+
+ // The current Android DNS resolver returns an UnknownHostException whenever network access
+ // is blocked. This can get cached in the current process-local InetAddress cache. Clearing
+ // the cache before attempting a connection ensures we never report a failure due to a
+ // negative cache entry.
+ InetAddress.clearDnsCache();
+
+ final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
+
+ final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
+ Log.d(TAG, "Running checkNetworkStatus() on thread "
+ + Thread.currentThread().getName() + " for UID " + getUid(context)
+ + "\n\tactiveNetworkInfo: " + networkInfo + "\n\tURL: " + address);
+ boolean checkStatus = false;
+ String checkDetails = "N/A";
+ try {
+ final URL url = new URL(address);
+ final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setReadTimeout(NETWORK_TIMEOUT_MS);
+ conn.setConnectTimeout(NETWORK_TIMEOUT_MS / 2);
+ conn.setRequestMethod("GET");
+ conn.connect();
+ final int response = conn.getResponseCode();
+ checkStatus = true;
+ checkDetails = "HTTP response for " + address + ": " + response;
+ } catch (Exception e) {
+ checkStatus = false;
+ checkDetails = "Exception getting " + address + ": " + e;
+ }
+ final NetworkCheckResult result = createNetworkCheckResult(checkStatus, checkDetails,
+ networkInfo);
+ Log.d(TAG, "Offering: " + result);
+ return result;
+ }
}
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
index 825f2c9..1fd3745 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
@@ -30,7 +30,6 @@
import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_DELETE;
import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_FULL_SCREEN;
import static com.android.cts.net.hostside.app2.Common.TAG;
-import static com.android.cts.net.hostside.app2.Common.getUid;
import android.app.Notification;
import android.app.Notification.Action;
@@ -42,15 +41,10 @@
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
-import java.net.HttpURLConnection;
-import java.net.InetAddress;
-import java.net.URL;
-
/**
* Receiver used to:
* <ol>
@@ -60,8 +54,6 @@
*/
public class MyBroadcastReceiver extends BroadcastReceiver {
- private static final int NETWORK_TIMEOUT_MS = 5 * 1000;
-
private final String mName;
public MyBroadcastReceiver() {
@@ -126,82 +118,6 @@
return String.valueOf(apiStatus);
}
- private static final String NETWORK_STATUS_TEMPLATE = "%s|%s|%s|%s|%s";
- /**
- * Checks whether the network is available and return a string which can then be send as a
- * result data for the ordered broadcast.
- *
- * <p>
- * The string has the following format:
- *
- * <p><pre><code>
- * NetinfoState|NetinfoDetailedState|RealConnectionCheck|RealConnectionCheckDetails|Netinfo
- * </code></pre>
- *
- * <p>Where:
- *
- * <ul>
- * <li>{@code NetinfoState}: enum value of {@link NetworkInfo.State}.
- * <li>{@code NetinfoDetailedState}: enum value of {@link NetworkInfo.DetailedState}.
- * <li>{@code RealConnectionCheck}: boolean value of a real connection check (i.e., an attempt
- * to access an external website.
- * <li>{@code RealConnectionCheckDetails}: if HTTP output core or exception string of the real
- * connection attempt
- * <li>{@code Netinfo}: string representation of the {@link NetworkInfo}.
- * </ul>
- *
- * For example, if the connection was established fine, the result would be something like:
- * <p><pre><code>
- * CONNECTED|CONNECTED|true|200|[type: WIFI[], state: CONNECTED/CONNECTED, reason: ...]
- * </code></pre>
- *
- */
- // TODO: now that it uses Binder, it counl return a Bundle with the data parts instead...
- static String checkNetworkStatus(Context context) {
- final ConnectivityManager cm =
- (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- // TODO: connect to a hostside server instead
- final String address = "http://example.com";
- final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
- Log.d(TAG, "Running checkNetworkStatus() on thread "
- + Thread.currentThread().getName() + " for UID " + getUid(context)
- + "\n\tactiveNetworkInfo: " + networkInfo + "\n\tURL: " + address);
- boolean checkStatus = false;
- String checkDetails = "N/A";
- try {
- final URL url = new URL(address);
- final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
- conn.setReadTimeout(NETWORK_TIMEOUT_MS);
- conn.setConnectTimeout(NETWORK_TIMEOUT_MS / 2);
- conn.setRequestMethod("GET");
- conn.setDoInput(true);
- conn.connect();
- final int response = conn.getResponseCode();
- checkStatus = true;
- checkDetails = "HTTP response for " + address + ": " + response;
- } catch (Exception e) {
- checkStatus = false;
- checkDetails = "Exception getting " + address + ": " + e;
- }
- // If the app tries to make a network connection in the foreground immediately after
- // trying to do the same when it's network access was blocked, it could receive a
- // UnknownHostException due to the cached DNS entry. So, clear the dns cache after
- // every network access for now until we have a fix on the platform side.
- InetAddress.clearDnsCache();
- Log.d(TAG, checkDetails);
- final String state, detailedState;
- if (networkInfo != null) {
- state = networkInfo.getState().name();
- detailedState = networkInfo.getDetailedState().name();
- } else {
- state = detailedState = "null";
- }
- final String status = String.format(NETWORK_STATUS_TEMPLATE, state, detailedState,
- Boolean.valueOf(checkStatus), checkDetails, networkInfo);
- Log.d(TAG, "Offering " + status);
- return status;
- }
-
/**
* Sends a system notification containing actions with pending intents to launch the app's
* main activitiy or service.
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
index 3ed5391..5010234 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
@@ -21,7 +21,6 @@
import static com.android.cts.net.hostside.app2.Common.ACTION_SNOOZE_WARNING;
import static com.android.cts.net.hostside.app2.Common.DYNAMIC_RECEIVER;
import static com.android.cts.net.hostside.app2.Common.TAG;
-import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_EXPORTED;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -41,6 +40,7 @@
import com.android.cts.net.hostside.IMyService;
import com.android.cts.net.hostside.INetworkCallback;
+import com.android.cts.net.hostside.NetworkCheckResult;
import com.android.modules.utils.build.SdkLevel;
/**
@@ -56,9 +56,7 @@
// TODO: move MyBroadcast static functions here - they were kept there to make git diff easier.
- private IMyService.Stub mBinder =
- new IMyService.Stub() {
-
+ private IMyService.Stub mBinder = new IMyService.Stub() {
@Override
public void registerBroadcastReceiver() {
if (mReceiver != null) {
@@ -83,8 +81,8 @@
}
@Override
- public String checkNetworkStatus() {
- return MyBroadcastReceiver.checkNetworkStatus(getApplicationContext());
+ public NetworkCheckResult checkNetworkStatus(String customUrl) {
+ return Common.checkNetworkStatus(getApplicationContext(), customUrl);
}
@Override
@@ -94,7 +92,7 @@
@Override
public void sendNotification(int notificationId, String notificationType) {
- MyBroadcastReceiver .sendNotification(getApplicationContext(), NOTIFICATION_CHANNEL_ID,
+ MyBroadcastReceiver.sendNotification(getApplicationContext(), NOTIFICATION_CHANNEL_ID,
notificationId, notificationType);
}
@@ -170,7 +168,7 @@
.getSystemService(JobScheduler.class);
return jobScheduler.schedule(jobInfo);
}
- };
+ };
@Override
public IBinder onBind(Intent intent) {
diff --git a/tests/cts/hostside/certs/Android.bp b/tests/cts/hostside/certs/Android.bp
index 60b5476..301973e 100644
--- a/tests/cts/hostside/certs/Android.bp
+++ b/tests/cts/hostside/certs/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/hostside/instrumentation_arguments/Android.bp b/tests/cts/hostside/instrumentation_arguments/Android.bp
new file mode 100644
index 0000000..cdede36
--- /dev/null
+++ b/tests/cts/hostside/instrumentation_arguments/Android.bp
@@ -0,0 +1,22 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+ name: "ArgumentConstants",
+ srcs: ["src/**/*.java"],
+}
diff --git a/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java b/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java
new file mode 100644
index 0000000..911b129
--- /dev/null
+++ b/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.arguments;
+
+public interface InstrumentationArguments {
+ String ARG_WAIVE_BIND_PRIORITY = "waive_bind_priority";
+ String ARG_CONNECTION_CHECK_CUSTOM_URL = "connection_check_custom_url";
+}
diff --git a/tests/cts/hostside/networkslicingtestapp/Android.bp b/tests/cts/hostside/networkslicingtestapp/Android.bp
index 2aa3f69..100b6e4 100644
--- a/tests/cts/hostside/networkslicingtestapp/Android.bp
+++ b/tests/cts/hostside/networkslicingtestapp/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -39,8 +40,8 @@
android_test_helper_app {
name: "CtsHostsideNetworkCapTestsAppWithoutProperty",
defaults: [
- "cts_support_defaults",
- "CtsHostsideNetworkCapTestsAppDefaults"
+ "cts_support_defaults",
+ "CtsHostsideNetworkCapTestsAppDefaults",
],
manifest: "AndroidManifestWithoutProperty.xml",
}
@@ -48,8 +49,8 @@
android_test_helper_app {
name: "CtsHostsideNetworkCapTestsAppWithProperty",
defaults: [
- "cts_support_defaults",
- "CtsHostsideNetworkCapTestsAppDefaults"
+ "cts_support_defaults",
+ "CtsHostsideNetworkCapTestsAppDefaults",
],
manifest: "AndroidManifestWithProperty.xml",
}
@@ -57,8 +58,8 @@
android_test_helper_app {
name: "CtsHostsideNetworkCapTestsAppSdk33",
defaults: [
- "cts_support_defaults",
- "CtsHostsideNetworkCapTestsAppDefaults"
+ "cts_support_defaults",
+ "CtsHostsideNetworkCapTestsAppDefaults",
],
target_sdk_version: "33",
manifest: "AndroidManifestWithoutProperty.xml",
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
index 849ac7c..fff716d 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
@@ -16,6 +16,8 @@
package com.android.cts.net;
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+
import android.platform.test.annotations.FlakyTest;
import com.android.testutils.SkipPresubmit;
@@ -26,9 +28,12 @@
import org.junit.Test;
+import java.util.Map;
+
@SkipPresubmit(reason = "Out of SLO flakiness")
public class HostsideConnOnActivityStartTest extends HostsideNetworkTestCase {
private static final String TEST_CLASS = TEST_PKG + ".ConnOnActivityStartTest";
+
@BeforeClassWithInfo
public static void setUpOnce(TestInformation testInfo) throws Exception {
uninstallPackage(testInfo, TEST_APP2_PKG, false);
@@ -42,22 +47,29 @@
@Test
public void testStartActivity_batterySaver() throws Exception {
- runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_batterySaver");
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_batterySaver");
}
@Test
public void testStartActivity_dataSaver() throws Exception {
- runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_dataSaver");
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_dataSaver");
}
@FlakyTest(bugId = 231440256)
@Test
public void testStartActivity_doze() throws Exception {
- runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_doze");
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_doze");
}
@Test
public void testStartActivity_appStandby() throws Exception {
- runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_appStandby");
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_appStandby");
+ }
+
+ // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
+ @Test
+ public void testStartActivity_default() throws Exception {
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_default",
+ Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
}
}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java
new file mode 100644
index 0000000..faabbef
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net;
+
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+
+import com.android.testutils.SkipPresubmit;
+import com.android.tradefed.device.DeviceNotAvailableException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+// TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side tests.
+@SkipPresubmit(reason = "Monitoring for flakiness")
+public class HostsideDefaultNetworkRestrictionsTests extends HostsideNetworkTestCase {
+ private static final String METERED_TEST_CLASS = TEST_PKG + ".DefaultRestrictionsMeteredTest";
+ private static final String NON_METERED_TEST_CLASS =
+ TEST_PKG + ".DefaultRestrictionsNonMeteredTest";
+
+ @Before
+ public void setUp() throws Exception {
+ uninstallPackage(TEST_APP2_PKG, false);
+ installPackage(TEST_APP2_APK);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ uninstallPackage(TEST_APP2_PKG, true);
+ }
+
+ private void runMeteredTest(String methodName) throws DeviceNotAvailableException {
+ runDeviceTestsWithCustomOptions(TEST_PKG, METERED_TEST_CLASS, methodName,
+ Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+ }
+
+ private void runNonMeteredTest(String methodName) throws DeviceNotAvailableException {
+ runDeviceTestsWithCustomOptions(TEST_PKG, NON_METERED_TEST_CLASS, methodName,
+ Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+ }
+
+ @Test
+ public void testMeteredNetworkAccess_defaultRestrictions_testActivityNetworkAccess()
+ throws Exception {
+ runMeteredTest("testActivityNetworkAccess");
+ }
+
+ @Test
+ public void testMeteredNetworkAccess_defaultRestrictions_testFgsNetworkAccess()
+ throws Exception {
+ runMeteredTest("testFgsNetworkAccess");
+ }
+
+ @Test
+ public void testMeteredNetworkAccess_defaultRestrictions_inFullAllowlist() throws Exception {
+ runMeteredTest("testBackgroundNetworkAccess_inFullAllowlist");
+ }
+
+ @Test
+ public void testMeteredNetworkAccess_defaultRestrictions_inExceptIdleAllowlist()
+ throws Exception {
+ runMeteredTest("testBackgroundNetworkAccess_inExceptIdleAllowlist");
+ }
+
+ @Test
+ public void testNonMeteredNetworkAccess_defaultRestrictions_testActivityNetworkAccess()
+ throws Exception {
+ runNonMeteredTest("testActivityNetworkAccess");
+ }
+
+ @Test
+ public void testNonMeteredNetworkAccess_defaultRestrictions_testFgsNetworkAccess()
+ throws Exception {
+ runNonMeteredTest("testFgsNetworkAccess");
+ }
+
+ @Test
+ public void testNonMeteredNetworkAccess_defaultRestrictions_inFullAllowlist() throws Exception {
+ runNonMeteredTest("testBackgroundNetworkAccess_inFullAllowlist");
+ }
+
+ @Test
+ public void testNonMeteredNetworkAccess_defaultRestrictions_inExceptIdleAllowlist()
+ throws Exception {
+ runNonMeteredTest("testBackgroundNetworkAccess_inExceptIdleAllowlist");
+ }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
index 04bd1ad..c4bcdfd 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
@@ -15,12 +15,16 @@
*/
package com.android.cts.net;
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+
import com.android.testutils.SkipPresubmit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import java.util.Map;
+
@SkipPresubmit(reason = "Out of SLO flakiness")
public class HostsideNetworkCallbackTests extends HostsideNetworkTestCase {
@@ -37,14 +41,21 @@
@Test
public void testOnBlockedStatusChanged_dataSaver() throws Exception {
- runDeviceTests(TEST_PKG,
+ runDeviceTestsWithCustomOptions(TEST_PKG,
TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_dataSaver");
}
@Test
public void testOnBlockedStatusChanged_powerSaver() throws Exception {
- runDeviceTests(TEST_PKG,
+ runDeviceTestsWithCustomOptions(TEST_PKG,
TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_powerSaver");
}
+
+ // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
+ @Test
+ public void testOnBlockedStatusChanged_default() throws Exception {
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".NetworkCallbackTest",
+ "testOnBlockedStatusChanged_default", Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+ }
}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
index 3ddb88b..4730b14 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
@@ -16,10 +16,14 @@
package com.android.cts.net;
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import java.util.Map;
+
public class HostsideNetworkPolicyManagerTests extends HostsideNetworkTestCase {
@Before
public void setUp() throws Exception {
@@ -34,41 +38,49 @@
@Test
public void testIsUidNetworkingBlocked_withUidNotBlocked() throws Exception {
- runDeviceTests(TEST_PKG,
+ runDeviceTestsWithCustomOptions(TEST_PKG,
TEST_PKG + ".NetworkPolicyManagerTest",
"testIsUidNetworkingBlocked_withUidNotBlocked");
}
@Test
public void testIsUidNetworkingBlocked_withSystemUid() throws Exception {
- runDeviceTests(TEST_PKG,
+ runDeviceTestsWithCustomOptions(TEST_PKG,
TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidNetworkingBlocked_withSystemUid");
}
@Test
public void testIsUidNetworkingBlocked_withDataSaverMode() throws Exception {
- runDeviceTests(TEST_PKG,
+ runDeviceTestsWithCustomOptions(TEST_PKG,
TEST_PKG + ".NetworkPolicyManagerTest",
"testIsUidNetworkingBlocked_withDataSaverMode");
}
@Test
public void testIsUidNetworkingBlocked_withRestrictedNetworkingMode() throws Exception {
- runDeviceTests(TEST_PKG,
+ runDeviceTestsWithCustomOptions(TEST_PKG,
TEST_PKG + ".NetworkPolicyManagerTest",
"testIsUidNetworkingBlocked_withRestrictedNetworkingMode");
}
@Test
public void testIsUidNetworkingBlocked_withPowerSaverMode() throws Exception {
- runDeviceTests(TEST_PKG,
+ runDeviceTestsWithCustomOptions(TEST_PKG,
TEST_PKG + ".NetworkPolicyManagerTest",
"testIsUidNetworkingBlocked_withPowerSaverMode");
}
@Test
public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
- runDeviceTests(TEST_PKG,
+ runDeviceTestsWithCustomOptions(TEST_PKG,
TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidRestrictedOnMeteredNetworks");
}
+
+ // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
+ @Test
+ public void testIsUidNetworkingBlocked_whenInBackground() throws Exception {
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".NetworkPolicyManagerTest",
+ "testIsUidNetworkingBlocked_whenInBackground",
+ Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
+ }
}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index 3358fd7..d7dfa80 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -16,12 +16,15 @@
package com.android.cts.net;
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_CONNECTION_CHECK_CUSTOM_URL;
+
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import com.android.ddmlib.Log;
import com.android.modules.utils.build.testing.DeviceSdkLevel;
+import com.android.tradefed.config.Option;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.targetprep.BuildError;
@@ -31,10 +34,13 @@
import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
+import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
import com.android.tradefed.util.RunUtil;
import org.junit.runner.RunWith;
+import java.util.Map;
+
@RunWith(DeviceJUnit4ClassRunner.class)
abstract class HostsideNetworkTestCase extends BaseHostJUnit4Test {
protected static final boolean DEBUG = false;
@@ -45,6 +51,10 @@
protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
protected static final String TEST_APP2_APK = "CtsHostsideNetworkTestsApp2.apk";
+ @Option(name = "custom-url", importance = Option.Importance.IF_UNSET,
+ description = "A custom url to use for testing network connections")
+ protected String mCustomUrl;
+
@BeforeClassWithInfo
public static void setUpOnceBase(TestInformation testInfo) throws Exception {
DeviceSdkLevel deviceSdkLevel = new DeviceSdkLevel(testInfo.getDevice());
@@ -146,6 +156,35 @@
+ packageName + ", u=" + currentUser);
}
+ protected boolean runDeviceTestsWithCustomOptions(String packageName, String className)
+ throws DeviceNotAvailableException {
+ return runDeviceTestsWithCustomOptions(packageName, className, null);
+ }
+
+ protected boolean runDeviceTestsWithCustomOptions(String packageName, String className,
+ String methodName) throws DeviceNotAvailableException {
+ return runDeviceTestsWithCustomOptions(packageName, className, methodName, null);
+ }
+
+ protected boolean runDeviceTestsWithCustomOptions(String packageName, String className,
+ String methodName, Map<String, String> testArgs) throws DeviceNotAvailableException {
+ final DeviceTestRunOptions deviceTestRunOptions = new DeviceTestRunOptions(packageName)
+ .setTestClassName(className)
+ .setTestMethodName(methodName);
+
+ // Currently there is only one custom option that the test exposes.
+ if (mCustomUrl != null) {
+ deviceTestRunOptions.addInstrumentationArg(ARG_CONNECTION_CHECK_CUSTOM_URL, mCustomUrl);
+ }
+ // Pass over any test specific arguments.
+ if (testArgs != null) {
+ for (Map.Entry<String, String> arg : testArgs.entrySet()) {
+ deviceTestRunOptions.addInstrumentationArg(arg.getKey(), arg.getValue());
+ }
+ }
+ return runDeviceTests(deviceTestRunOptions);
+ }
+
protected String runCommand(String command) throws DeviceNotAvailableException {
Log.d(TAG, "Command: '" + command + "'");
final String output = getDevice().executeShellCommand(command);
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
index 9c3751d..7b9d3b5 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
@@ -46,7 +46,7 @@
@SecurityTest
@Test
public void testDataWarningReceiver() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".DataWarningReceiverTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataWarningReceiverTest",
"testSnoozeWarningNotReceived");
}
@@ -56,25 +56,25 @@
@Test
public void testDataSaverMode_disabled() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
"testGetRestrictBackgroundStatus_disabled");
}
@Test
public void testDataSaverMode_whitelisted() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
"testGetRestrictBackgroundStatus_whitelisted");
}
@Test
public void testDataSaverMode_enabled() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
"testGetRestrictBackgroundStatus_enabled");
}
@Test
public void testDataSaverMode_blacklisted() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
"testGetRestrictBackgroundStatus_blacklisted");
}
@@ -97,13 +97,13 @@
@Test
public void testDataSaverMode_requiredWhitelistedPackages() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
"testGetRestrictBackgroundStatus_requiredWhitelistedPackages");
}
@Test
public void testDataSaverMode_broadcastNotSentOnUnsupportedDevices() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
"testBroadcastNotSentOnUnsupportedDevices");
}
@@ -113,19 +113,19 @@
@Test
public void testBatterySaverModeMetered_disabled() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
"testBackgroundNetworkAccess_disabled");
}
@Test
public void testBatterySaverModeMetered_whitelisted() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
"testBackgroundNetworkAccess_whitelisted");
}
@Test
public void testBatterySaverModeMetered_enabled() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
"testBackgroundNetworkAccess_enabled");
}
@@ -149,19 +149,19 @@
@Test
public void testBatterySaverModeNonMetered_disabled() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
"testBackgroundNetworkAccess_disabled");
}
@Test
public void testBatterySaverModeNonMetered_whitelisted() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
"testBackgroundNetworkAccess_whitelisted");
}
@Test
public void testBatterySaverModeNonMetered_enabled() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
"testBackgroundNetworkAccess_enabled");
}
@@ -171,31 +171,31 @@
@Test
public void testAppIdleMetered_disabled() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
"testBackgroundNetworkAccess_disabled");
}
@Test
public void testAppIdleMetered_whitelisted() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
"testBackgroundNetworkAccess_whitelisted");
}
@Test
public void testAppIdleMetered_tempWhitelisted() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
"testBackgroundNetworkAccess_tempWhitelisted");
}
@Test
public void testAppIdleMetered_enabled() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
"testBackgroundNetworkAccess_enabled");
}
@Test
public void testAppIdleMetered_idleWhitelisted() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
"testAppIdleNetworkAccess_idleWhitelisted");
}
@@ -206,51 +206,51 @@
@Test
public void testAppIdleNonMetered_disabled() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
"testBackgroundNetworkAccess_disabled");
}
@Test
public void testAppIdleNonMetered_whitelisted() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
"testBackgroundNetworkAccess_whitelisted");
}
@Test
public void testAppIdleNonMetered_tempWhitelisted() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
"testBackgroundNetworkAccess_tempWhitelisted");
}
@Test
public void testAppIdleNonMetered_enabled() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
"testBackgroundNetworkAccess_enabled");
}
@Test
public void testAppIdleNonMetered_idleWhitelisted() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
"testAppIdleNetworkAccess_idleWhitelisted");
}
@Test
public void testAppIdleNonMetered_whenCharging() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
"testAppIdleNetworkAccess_whenCharging");
}
@Test
public void testAppIdleMetered_whenCharging() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
"testAppIdleNetworkAccess_whenCharging");
}
@Test
public void testAppIdle_toast() throws Exception {
// Check that showing a toast doesn't bring an app out of standby
- runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
"testAppIdle_toast");
}
@@ -260,25 +260,25 @@
@Test
public void testDozeModeMetered_disabled() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
"testBackgroundNetworkAccess_disabled");
}
@Test
public void testDozeModeMetered_whitelisted() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
"testBackgroundNetworkAccess_whitelisted");
}
@Test
public void testDozeModeMetered_enabled() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
"testBackgroundNetworkAccess_enabled");
}
@Test
public void testDozeModeMetered_enabledButWhitelistedOnNotificationAction() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
"testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
}
@@ -289,26 +289,26 @@
@Test
public void testDozeModeNonMetered_disabled() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
"testBackgroundNetworkAccess_disabled");
}
@Test
public void testDozeModeNonMetered_whitelisted() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
"testBackgroundNetworkAccess_whitelisted");
}
@Test
public void testDozeModeNonMetered_enabled() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
"testBackgroundNetworkAccess_enabled");
}
@Test
public void testDozeModeNonMetered_enabledButWhitelistedOnNotificationAction()
throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
"testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
}
@@ -318,55 +318,55 @@
@Test
public void testDataAndBatterySaverModes_meteredNetwork() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
"testDataAndBatterySaverModes_meteredNetwork");
}
@Test
public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
"testDataAndBatterySaverModes_nonMeteredNetwork");
}
@Test
public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
"testDozeAndBatterySaverMode_powerSaveWhitelists");
}
@Test
public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
"testDozeAndAppIdle_powerSaveWhitelists");
}
@Test
public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
"testAppIdleAndDoze_tempPowerSaveWhitelists");
}
@Test
public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
"testAppIdleAndBatterySaver_tempPowerSaveWhitelists");
}
@Test
public void testDozeAndAppIdle_appIdleWhitelist() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
"testDozeAndAppIdle_appIdleWhitelist");
}
@Test
public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
"testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists");
}
@Test
public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
"testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists");
}
@@ -376,13 +376,13 @@
@Test
public void testNetworkAccess_restrictedMode() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
"testNetworkAccess");
}
@Test
public void testNetworkAccess_restrictedMode_withBatterySaver() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
"testNetworkAccess_withBatterySaver");
}
@@ -392,12 +392,12 @@
@Test
public void testMeteredNetworkAccess_expeditedJob() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".ExpeditedJobMeteredTest");
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".ExpeditedJobMeteredTest");
}
@Test
public void testNonMeteredNetworkAccess_expeditedJob() throws Exception {
- runDeviceTests(TEST_PKG, TEST_PKG + ".ExpeditedJobNonMeteredTest");
+ runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".ExpeditedJobNonMeteredTest");
}
/*******************
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
index 4f21af7..f0a87af 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
@@ -171,4 +171,16 @@
public void testSetVpnDefaultForUids() throws Exception {
runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testSetVpnDefaultForUids");
}
+
+ @Test
+ public void testDropPacketToVpnAddress_WithoutDuplicatedAddress() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest",
+ "testDropPacketToVpnAddress_WithoutDuplicatedAddress");
+ }
+
+ @Test
+ public void testDropPacketToVpnAddress_WithDuplicatedAddress() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest",
+ "testDropPacketToVpnAddress_WithDuplicatedAddress");
+ }
}
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/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
new file mode 100644
index 0000000..5ac4229
--- /dev/null
+++ b/tests/cts/multidevices/Android.bp
@@ -0,0 +1,42 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+python_test_host {
+ name: "CtsConnectivityMultiDevicesTestCases",
+ main: "connectivity_multi_devices_test.py",
+ srcs: ["connectivity_multi_devices_test.py"],
+ libs: [
+ "mobly",
+ ],
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+ test_options: {
+ unit_test: false,
+ },
+ data: [
+ // Package the snippet with the mobly test
+ ":connectivity_multi_devices_snippet",
+ ],
+ version: {
+ py3: {
+ embedded_launcher: true,
+ },
+ },
+}
diff --git a/tests/cts/multidevices/AndroidTest.xml b/tests/cts/multidevices/AndroidTest.xml
new file mode 100644
index 0000000..5312b4d
--- /dev/null
+++ b/tests/cts/multidevices/AndroidTest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Config for CTS Connectivity multi devices test cases">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="networking" />
+ <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+
+ <device name="device1">
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="connectivity_multi_devices_snippet.apk" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ </target_preparer>
+ </device>
+ <device name="device2">
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="connectivity_multi_devices_snippet.apk" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ </target_preparer>
+ </device>
+
+ <test class="com.android.tradefed.testtype.mobly.MoblyBinaryHostTest">
+ <!-- The mobly-par-file-name should match the module name -->
+ <option name="mobly-par-file-name" value="CtsConnectivityMultiDevicesTestCases" />
+ <!-- Timeout limit in milliseconds for all test cases of the python binary -->
+ <option name="mobly-test-timeout" value="180000" />
+ </test>
+</configuration>
+
diff --git a/tests/cts/multidevices/connectivity_multi_devices_test.py b/tests/cts/multidevices/connectivity_multi_devices_test.py
new file mode 100644
index 0000000..ab885049
--- /dev/null
+++ b/tests/cts/multidevices/connectivity_multi_devices_test.py
@@ -0,0 +1,110 @@
+# Lint as: python3
+"""Connectivity multi devices tests."""
+import base64
+import sys
+import uuid
+
+from mobly import asserts
+from mobly import base_test
+from mobly import test_runner
+from mobly import utils
+from mobly.controllers import android_device
+
+CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE = "com.google.snippet.connectivity"
+
+
+class UpstreamType:
+ CELLULAR = 1
+ WIFI = 2
+
+
+class ConnectivityMultiDevicesTest(base_test.BaseTestClass):
+
+ def setup_class(self):
+ # Declare that two Android devices are needed.
+ self.clientDevice, self.serverDevice = self.register_controller(
+ android_device, min_number=2
+ )
+
+ def setup_device(device):
+ device.load_snippet(
+ "connectivity_multi_devices_snippet",
+ CONNECTIVITY_MULTI_DEVICES_SNIPPET_PACKAGE,
+ )
+
+ # Set up devices in parallel to save time.
+ utils.concurrent_exec(
+ setup_device,
+ ((self.clientDevice,), (self.serverDevice,)),
+ max_workers=2,
+ raise_on_exception=True,
+ )
+
+ @staticmethod
+ def generate_uuid32_base64():
+ """Generates a UUID32 and encodes it in Base64.
+
+ Returns:
+ str: The Base64-encoded UUID32 string. Which is 22 characters.
+ """
+ return base64.b64encode(uuid.uuid1().bytes).decode("utf-8").strip("=")
+
+ def _do_test_hotspot_for_upstream_type(self, upstream_type):
+ """Test hotspot with the specified upstream type.
+
+ This test create a hotspot, make the client connect
+ to it, and verify the packet is forwarded by the hotspot.
+ """
+ server = self.serverDevice.connectivity_multi_devices_snippet
+ client = self.clientDevice.connectivity_multi_devices_snippet
+
+ # Assert pre-conditions specific to each upstream type.
+ asserts.skip_if(not client.hasWifiFeature(), "Client requires Wifi feature")
+ asserts.skip_if(
+ not server.hasHotspotFeature(), "Server requires hotspot feature"
+ )
+ if upstream_type == UpstreamType.CELLULAR:
+ asserts.skip_if(
+ not server.hasTelephonyFeature(), "Server requires Telephony feature"
+ )
+ server.requestCellularAndEnsureDefault()
+ elif upstream_type == UpstreamType.WIFI:
+ asserts.skip_if(
+ not server.isStaApConcurrencySupported(),
+ "Server requires Wifi AP + STA concurrency",
+ )
+ server.ensureWifiIsDefault()
+ else:
+ raise ValueError(f"Invalid upstream type: {upstream_type}")
+
+ # Generate ssid/passphrase with random characters to make sure nearby devices won't
+ # connect unexpectedly. Note that total length of ssid cannot go over 32.
+ testSsid = "HOTSPOT-" + self.generate_uuid32_base64()
+ testPassphrase = self.generate_uuid32_base64()
+
+ try:
+ # Create a hotspot with fixed SSID and password.
+ server.startHotspot(testSsid, testPassphrase)
+
+ # Make the client connects to the hotspot.
+ client.connectToWifi(testSsid, testPassphrase, True)
+
+ finally:
+ if upstream_type == UpstreamType.CELLULAR:
+ server.unrequestCellular()
+ # Teardown the hotspot.
+ server.stopAllTethering()
+
+ def test_hotspot_upstream_wifi(self):
+ self._do_test_hotspot_for_upstream_type(UpstreamType.WIFI)
+
+ def test_hotspot_upstream_cellular(self):
+ self._do_test_hotspot_for_upstream_type(UpstreamType.CELLULAR)
+
+
+if __name__ == "__main__":
+ # Take test args
+ if "--" in sys.argv:
+ index = sys.argv.index("--")
+ sys.argv = sys.argv[:1] + sys.argv[index + 1 :]
+ test_runner.main()
diff --git a/tests/cts/multidevices/snippet/Android.bp b/tests/cts/multidevices/snippet/Android.bp
new file mode 100644
index 0000000..5940cbb
--- /dev/null
+++ b/tests/cts/multidevices/snippet/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+ name: "connectivity_multi_devices_snippet",
+ defaults: [
+ "ConnectivityTestsLatestSdkDefaults",
+ "cts_defaults",
+ "framework-connectivity-test-defaults",
+ ],
+ srcs: [
+ "ConnectivityMultiDevicesSnippet.kt",
+ ],
+ manifest: "AndroidManifest.xml",
+ static_libs: [
+ "androidx.test.runner",
+ "mobly-snippet-lib",
+ "cts-net-utils",
+ ],
+ platform_apis: true,
+ min_sdk_version: "30", // R
+}
diff --git a/tests/cts/multidevices/snippet/AndroidManifest.xml b/tests/cts/multidevices/snippet/AndroidManifest.xml
new file mode 100644
index 0000000..9ed8146
--- /dev/null
+++ b/tests/cts/multidevices/snippet/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.snippet.connectivity">
+ <!-- Declare the minimum Android SDK version and internet permission,
+ which are required by Mobly Snippet Lib since it uses network socket. -->
+ <uses-sdk android:minSdkVersion="30" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <application>
+ <!-- Add any classes that implement the Snippet interface as meta-data, whose
+ value is a comma-separated string, each section being the package path
+ of a snippet class -->
+ <meta-data
+ android:name="mobly-snippets"
+ android:value="com.google.snippet.connectivity.ConnectivityMultiDevicesSnippet" />
+ </application>
+ <!-- Add an instrumentation tag so that the app can be launched through an
+ instrument command. The runner `com.google.android.mobly.snippet.SnippetRunner`
+ is derived from `AndroidJUnitRunner`, and is required to use the
+ Mobly Snippet Lib. -->
+ <instrumentation
+ android:name="com.google.android.mobly.snippet.SnippetRunner"
+ android:targetPackage="com.google.snippet.connectivity" />
+</manifest>
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
new file mode 100644
index 0000000..c883b78
--- /dev/null
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -0,0 +1,176 @@
+/*
+ * 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.google.snippet.connectivity
+
+import android.Manifest.permission.OVERRIDE_WIFI_CONFIG
+import android.content.pm.PackageManager.FEATURE_TELEPHONY
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.cts.util.CtsNetUtils
+import android.net.cts.util.CtsTetheringUtils
+import android.net.wifi.ScanResult
+import android.net.wifi.SoftApConfiguration
+import android.net.wifi.SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
+import android.net.wifi.WifiConfiguration
+import android.net.wifi.WifiManager
+import android.net.wifi.WifiNetworkSpecifier
+import android.net.wifi.WifiSsid
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.ConnectUtil
+import com.android.testutils.NetworkCallbackHelper
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
+import com.google.android.mobly.snippet.Snippet
+import com.google.android.mobly.snippet.rpc.Rpc
+
+class ConnectivityMultiDevicesSnippet : Snippet {
+ private val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
+ private val wifiManager = context.getSystemService(WifiManager::class.java)!!
+ private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+ private val pm = context.packageManager
+ private val ctsNetUtils = CtsNetUtils(context)
+ private val cbHelper = NetworkCallbackHelper()
+ private val ctsTetheringUtils = CtsTetheringUtils(context)
+ private var oldSoftApConfig: SoftApConfiguration? = null
+
+ override fun shutdown() {
+ cbHelper.unregisterAll()
+ }
+
+ @Rpc(description = "Check whether the device has wifi feature.")
+ fun hasWifiFeature() = pm.hasSystemFeature(FEATURE_WIFI)
+
+ @Rpc(description = "Check whether the device has telephony feature.")
+ fun hasTelephonyFeature() = pm.hasSystemFeature(FEATURE_TELEPHONY)
+
+ @Rpc(description = "Check whether the device supporters AP + STA concurrency.")
+ fun isStaApConcurrencySupported() {
+ wifiManager.isStaApConcurrencySupported()
+ }
+
+ @Rpc(description = "Request cellular connection and ensure it is the default network.")
+ fun requestCellularAndEnsureDefault() {
+ ctsNetUtils.disableWifi()
+ val network = cbHelper.requestCell()
+ ctsNetUtils.expectNetworkIsSystemDefault(network)
+ }
+
+ @Rpc(description = "Unrequest cellular connection.")
+ fun unrequestCellular() {
+ cbHelper.unrequestCell()
+ }
+
+ @Rpc(description = "Ensure any wifi is connected and is the default network.")
+ fun ensureWifiIsDefault() {
+ val network = ctsNetUtils.ensureWifiConnected()
+ ctsNetUtils.expectNetworkIsSystemDefault(network)
+ }
+
+ @Rpc(description = "Connect to specified wifi network.")
+ // Suppress warning because WifiManager methods to connect to a config are
+ // documented not to be deprecated for privileged users.
+ @Suppress("DEPRECATION")
+ fun connectToWifi(ssid: String, passphrase: String, requireValidation: Boolean): Network {
+ val specifier = WifiNetworkSpecifier.Builder()
+ .setSsid(ssid)
+ .setWpa2Passphrase(passphrase)
+ .setBand(ScanResult.WIFI_BAND_24_GHZ)
+ .build()
+ val wifiConfig = WifiConfiguration()
+ wifiConfig.SSID = "\"" + ssid + "\""
+ wifiConfig.preSharedKey = "\"" + passphrase + "\""
+ wifiConfig.hiddenSSID = true
+ wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA2_PSK)
+ wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
+ wifiConfig.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
+
+ // Register network callback for the specific wifi.
+ val networkCallback = TestableNetworkCallback()
+ val wifiRequest = NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI)
+ .setNetworkSpecifier(specifier)
+ .build()
+ cm.registerNetworkCallback(wifiRequest, networkCallback)
+
+ try {
+ // Add the test configuration and connect to it.
+ val connectUtil = ConnectUtil(context)
+ connectUtil.connectToWifiConfig(wifiConfig)
+
+ val event = networkCallback.expect<Available>()
+ if (requireValidation) {
+ networkCallback.eventuallyExpect<CapabilitiesChanged> {
+ it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+ }
+ }
+ return event.network
+ } finally {
+ cm.unregisterNetworkCallback(networkCallback)
+ }
+ }
+
+ @Rpc(description = "Check whether the device supports hotspot feature.")
+ fun hasHotspotFeature(): Boolean {
+ val tetheringCallback = ctsTetheringUtils.registerTetheringEventCallback()
+ try {
+ return tetheringCallback.isWifiTetheringSupported(context)
+ } finally {
+ ctsTetheringUtils.unregisterTetheringEventCallback(tetheringCallback)
+ }
+ }
+
+ @Rpc(description = "Start a hotspot with given SSID and passphrase.")
+ fun startHotspot(ssid: String, passphrase: String) {
+ // Store old config.
+ runAsShell(OVERRIDE_WIFI_CONFIG) {
+ oldSoftApConfig = wifiManager.getSoftApConfiguration()
+ }
+
+ val softApConfig = SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes(ssid.toByteArray()))
+ .setPassphrase(passphrase, SECURITY_TYPE_WPA2_PSK)
+ .setBand(SoftApConfiguration.BAND_2GHZ)
+ .build()
+ runAsShell(OVERRIDE_WIFI_CONFIG) {
+ wifiManager.setSoftApConfiguration(softApConfig)
+ }
+ val tetheringCallback = ctsTetheringUtils.registerTetheringEventCallback()
+ try {
+ tetheringCallback.expectNoTetheringActive()
+ ctsTetheringUtils.startWifiTethering(tetheringCallback)
+ } finally {
+ ctsTetheringUtils.unregisterTetheringEventCallback(tetheringCallback)
+ }
+ }
+
+ @Rpc(description = "Stop all tethering.")
+ fun stopAllTethering() {
+ ctsTetheringUtils.stopAllTethering()
+
+ // Restore old config.
+ oldSoftApConfig?.let {
+ runAsShell(OVERRIDE_WIFI_CONFIG) {
+ wifiManager.setSoftApConfiguration(it)
+ }
+ }
+ }
+}
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 3d53d6c..074c587 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -54,27 +55,26 @@
"junit",
"junit-params",
"modules-utils-build",
+ "net-tests-utils",
"net-utils-framework-common",
"truth",
"TetheringIntegrationTestsBaseLib",
],
- // uncomment when b/13249961 is fixed
- // sdk_version: "current",
- platform_apis: true,
+ min_sdk_version: "30",
per_testcase_directory: true,
host_required: ["net-tests-utils-host-common"],
test_config_template: "AndroidTestTemplate.xml",
data: [
":ConnectivityTestPreparer",
":CtsCarrierServicePackage",
- ]
+ ],
}
// Networking CTS tests for development and release. These tests always target the platform SDK
// version, and are subject to all the restrictions appropriate to that version. Before SDK
-// finalization, these tests have a min_sdk_version of 10000, and cannot be installed on release
-// devices.
+// finalization, these tests have a min_sdk_version of 10000, but they can still be installed on
+// release devices as their min_sdk_version is set to a production version.
android_test {
name: "CtsNetTestCases",
defaults: [
@@ -87,6 +87,14 @@
],
test_suites: [
"cts",
+ "mts-dnsresolver",
+ "mts-networking",
+ "mts-tethering",
+ "mts-wifi",
+ "mcts-dnsresolver",
+ "mcts-networking",
+ "mcts-tethering",
+ "mcts-wifi",
"general-tests",
],
}
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 1f1dd5d..2ec3a70 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java
index 8d68c5f..af1af43 100644
--- a/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java
+++ b/tests/cts/net/api23Test/src/android/net/cts/api23test/ConnectivityManagerApi23Test.java
@@ -65,7 +65,7 @@
}
ConnectivityReceiver.prepare();
- mCtsNetUtils.toggleWifi();
+ mCtsNetUtils.reconnectWifiAndWaitForConnectivityAction();
// The connectivity broadcast has been sent; push through a terminal broadcast
// to wait for in the receive to confirm it didn't see the connectivity change.
@@ -88,7 +88,7 @@
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
Thread.sleep(200);
- mCtsNetUtils.toggleWifi();
+ mCtsNetUtils.reconnectWifiAndWaitForConnectivityAction();
Intent getConnectivityCount = new Intent(GET_WIFI_CONNECTIVITY_ACTION_COUNT);
assertEquals(2, sendOrderedBroadcastAndReturnResultCode(
@@ -106,7 +106,7 @@
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
mContext.registerReceiver(receiver, filter);
- mCtsNetUtils.toggleWifi();
+ mCtsNetUtils.reconnectWifiAndWaitForConnectivityAction();
Intent finalIntent = new Intent(ConnectivityReceiver.FINAL_ACTION);
finalIntent.setClass(mContext, ConnectivityReceiver.class);
mContext.sendBroadcast(finalIntent);
diff --git a/tests/cts/net/appForApi23/Android.bp b/tests/cts/net/appForApi23/Android.bp
index b39690f..d300743 100644
--- a/tests/cts/net/appForApi23/Android.bp
+++ b/tests/cts/net/appForApi23/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/net/jni/Android.bp b/tests/cts/net/jni/Android.bp
index a421349..fbf4f29 100644
--- a/tests/cts/net/jni/Android.bp
+++ b/tests/cts/net/jni/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/net/native/Android.bp b/tests/cts/net/native/Android.bp
index 153ff51..3f24592 100644
--- a/tests/cts/net/native/Android.bp
+++ b/tests/cts/net/native/Android.bp
@@ -15,6 +15,7 @@
// Build the unit tests.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
index a9e3715..8e24fba 100644
--- a/tests/cts/net/native/dns/Android.bp
+++ b/tests/cts/net/native/dns/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
new file mode 100644
index 0000000..3be44f7
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -0,0 +1,239 @@
+/*
+ * 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.
+ */
+// ktlint does not allow annotating function argument literals inline. Disable the specific rule
+// since this negatively affects readability.
+@file:Suppress("ktlint:standard:comment-wrapping")
+
+package android.net.cts
+
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.apf.ApfCapabilities
+import android.os.Build
+import android.os.PowerManager
+import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig
+import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
+import android.system.OsConstants
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.PropertyUtil.getVsrApiLevel
+import com.android.compatibility.common.util.SystemUtil.runShellCommand
+import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
+import com.android.internal.util.HexDump
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.NetworkStackModuleTest
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.SkipPresubmit
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import com.google.common.truth.TruthJUnit.assume
+import java.lang.Thread
+import kotlin.random.Random
+import kotlin.test.assertNotNull
+import org.junit.After
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TAG = "ApfIntegrationTest"
+private const val TIMEOUT_MS = 2000L
+private const val APF_NEW_RA_FILTER_VERSION = "apf_new_ra_filter_version"
+private const val POLLING_INTERVAL_MS: Int = 100
+
+@AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
+@RunWith(DevSdkIgnoreRunner::class)
+@NetworkStackModuleTest
+class ApfIntegrationTest {
+ companion object {
+ @BeforeClass
+ @JvmStatic
+ @Suppress("ktlint:standard:no-multi-spaces")
+ fun setupOnce() {
+ // TODO: check that there is no active wifi network. Otherwise, ApfFilter has already been
+ // created.
+ // APF adb cmds are only implemented in ApfFilter.java. Enable experiment to prevent
+ // LegacyApfFilter.java from being used.
+ runAsShell(WRITE_DEVICE_CONFIG) {
+ DeviceConfig.setProperty(
+ NAMESPACE_CONNECTIVITY,
+ APF_NEW_RA_FILTER_VERSION,
+ "1", // value => force enabled
+ false // makeDefault
+ )
+ }
+ }
+ }
+
+ @get:Rule
+ val ignoreRule = DevSdkIgnoreRule()
+
+ private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
+ private val pm by lazy { context.packageManager }
+ private val powerManager by lazy { context.getSystemService(PowerManager::class.java)!! }
+ private val wakeLock by lazy { powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG) }
+ private lateinit var ifname: String
+ private lateinit var networkCallback: TestableNetworkCallback
+ private lateinit var caps: ApfCapabilities
+
+ fun getApfCapabilities(): ApfCapabilities {
+ val caps = runShellCommand("cmd network_stack apf $ifname capabilities").trim()
+ if (caps.isEmpty()) {
+ return ApfCapabilities(0, 0, 0)
+ }
+ val (version, maxLen, packetFormat) = caps.split(",").map { it.toInt() }
+ return ApfCapabilities(version, maxLen, packetFormat)
+ }
+
+ fun pollingCheck(condition: () -> Boolean, timeout_ms: Int): Boolean {
+ var polling_time = 0
+ do {
+ Thread.sleep(POLLING_INTERVAL_MS.toLong())
+ polling_time += POLLING_INTERVAL_MS
+ if (condition()) return true
+ } while (polling_time < timeout_ms)
+ return false
+ }
+
+ fun turnScreenOff() {
+ if (!wakeLock.isHeld()) wakeLock.acquire()
+ runShellCommandOrThrow("input keyevent KEYCODE_SLEEP")
+ val result = pollingCheck({ !powerManager.isInteractive() }, timeout_ms = 2000)
+ assertThat(result).isTrue()
+ }
+
+ fun turnScreenOn() {
+ if (wakeLock.isHeld()) wakeLock.release()
+ runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP")
+ val result = pollingCheck({ powerManager.isInteractive() }, timeout_ms = 2000)
+ assertThat(result).isTrue()
+ }
+
+ @Before
+ fun setUp() {
+ assume().that(pm.hasSystemFeature(FEATURE_WIFI)).isTrue()
+ // APF must run when the screen is off and the device is not interactive.
+ // TODO: consider running some of the tests with screen on (capabilities, read / write).
+ turnScreenOff()
+
+ networkCallback = TestableNetworkCallback()
+ cm.requestNetwork(
+ NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build(),
+ networkCallback
+ )
+ networkCallback.eventuallyExpect<LinkPropertiesChanged>(TIMEOUT_MS) {
+ ifname = assertNotNull(it.lp.interfaceName)
+ true
+ }
+ // It's possible the device does not support APF, in which case this command will not be
+ // successful. Ignore the error as testApfCapabilities() already asserts APF support on the
+ // respective VSR releases and all other tests are based on the capabilities indicated.
+ runShellCommand("cmd network_stack apf $ifname pause")
+ caps = getApfCapabilities()
+ }
+
+ @After
+ fun tearDown() {
+ if (::ifname.isInitialized) {
+ runShellCommand("cmd network_stack apf $ifname resume")
+ }
+ if (::networkCallback.isInitialized) {
+ cm.unregisterNetworkCallback(networkCallback)
+ }
+ turnScreenOn()
+ }
+
+ @Test
+ fun testApfCapabilities() {
+ // APF became mandatory in Android 14 VSR.
+ assume().that(getVsrApiLevel()).isAtLeast(34)
+
+ // ApfFilter does not support anything but ARPHRD_ETHER.
+ assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
+
+ // DEVICEs launching with Android 14 with CHIPSETs that set ro.board.first_api_level to 34:
+ // - [GMS-VSR-5.3.12-003] MUST return 4 or higher as the APF version number from calls to
+ // the getApfPacketFilterCapabilities HAL method.
+ // - [GMS-VSR-5.3.12-004] MUST indicate at least 1024 bytes of usable memory from calls to
+ // the getApfPacketFilterCapabilities HAL method.
+ // TODO: check whether above text should be changed "34 or higher"
+ // This should assert apfVersionSupported >= 4 as per the VSR requirements, but there are
+ // currently no tests for APFv6 and there cannot be a valid implementation as the
+ // interpreter has yet to be finalized.
+ assertThat(caps.apfVersionSupported).isEqualTo(4)
+ assertThat(caps.maximumApfProgramSize).isAtLeast(1024)
+
+ // DEVICEs launching with Android 15 (AOSP experimental) or higher with CHIPSETs that set
+ // ro.board.first_api_level or ro.board.api_level to 202404 or higher:
+ // - [GMS-VSR-5.3.12-009] MUST indicate at least 2000 bytes of usable memory from calls to
+ // the getApfPacketFilterCapabilities HAL method.
+ if (getVsrApiLevel() >= 202404) {
+ assertThat(caps.maximumApfProgramSize).isAtLeast(2000)
+ }
+ }
+
+ // APF is backwards compatible, i.e. a v6 interpreter supports both v2 and v4 functionality.
+ fun assumeApfVersionSupportAtLeast(version: Int) {
+ assume().that(caps.apfVersionSupported).isAtLeast(version)
+ }
+
+ fun installProgram(bytes: ByteArray) {
+ val prog = HexDump.toHexString(bytes, 0 /* offset */, bytes.size, false /* upperCase */)
+ val result = runShellCommandOrThrow("cmd network_stack apf $ifname install $prog").trim()
+ // runShellCommandOrThrow only throws on S+.
+ assertThat(result).isEqualTo("success")
+ }
+
+ fun readProgram(): ByteArray {
+ val progHexString = runShellCommandOrThrow("cmd network_stack apf $ifname read").trim()
+ // runShellCommandOrThrow only throws on S+.
+ assertThat(progHexString).isNotEmpty()
+ return HexDump.hexStringToByteArray(progHexString)
+ }
+
+ @SkipPresubmit(reason = "This test takes longer than 1 minute, do not run it on presubmit.")
+ // APF integration is mostly broken before V, only run the full read / write test on V+.
+ @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testReadWriteProgram() {
+ assumeApfVersionSupportAtLeast(4)
+
+ // Only test down to 2 bytes. The first byte always stays PASS.
+ val program = ByteArray(caps.maximumApfProgramSize)
+ for (i in caps.maximumApfProgramSize downTo 2) {
+ // Randomize bytes in range [1, i). And install first [0, i) bytes of program.
+ // Note that only the very first instruction (PASS) is valid APF bytecode.
+ Random.nextBytes(program, 1 /* fromIndex */, i /* toIndex */)
+ installProgram(program.sliceArray(0..<i))
+
+ // Compare entire memory region.
+ val readResult = readProgram()
+ assertWithMessage("read/write $i byte prog failed").that(readResult).isEqualTo(program)
+ }
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java b/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
index 466514c..16a7b73 100644
--- a/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/BatteryStatsManagerTest.java
@@ -48,6 +48,7 @@
import androidx.test.filters.SdkSuppress;
import androidx.test.runner.AndroidJUnit4;
+import com.android.testutils.AutoReleaseNetworkCallbackRule;
import com.android.testutils.DevSdkIgnoreRule;
import org.junit.Before;
@@ -67,7 +68,10 @@
@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) // BatteryStatsManager did not exist on Q
public class BatteryStatsManagerTest{
- @Rule
+ @Rule(order = 1)
+ public final AutoReleaseNetworkCallbackRule
+ networkCallbackRule = new AutoReleaseNetworkCallbackRule();
+ @Rule(order = 2)
public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
private static final String TAG = BatteryStatsManagerTest.class.getSimpleName();
private static final String TEST_URL = "https://connectivitycheck.gstatic.com/generate_204";
@@ -145,7 +149,7 @@
return;
}
- final Network cellNetwork = mCtsNetUtils.connectToCell();
+ final Network cellNetwork = networkCallbackRule.requestCell();
final URL url = new URL(TEST_URL);
// Get cellular battery stats
@@ -199,7 +203,8 @@
Log.d(TAG, "Generate traffic on wifi network.");
generateNetworkTraffic(wifiNetwork, url);
// Wifi battery stats are updated when wifi on.
- mCtsNetUtils.toggleWifi();
+ mCtsNetUtils.disableWifi();
+ mCtsNetUtils.ensureWifiConnected();
// Check wifi battery stats are updated.
runAsShell(UPDATE_DEVICE_STATS,
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index 99222dd..07e2024 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -47,6 +47,7 @@
import com.android.modules.utils.build.SdkLevel.isAtLeastR
import com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTPS_URL
import com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTP_URL
+import com.android.testutils.AutoReleaseNetworkCallbackRule
import com.android.testutils.DeviceConfigRule
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
import com.android.testutils.SkipMainlinePresubmit
@@ -101,9 +102,12 @@
private val server = TestHttpServer("localhost")
- @get:Rule
+ @get:Rule(order = 1)
val deviceConfigRule = DeviceConfigRule(retryCountBeforeSIfConfigChanged = 5)
+ @get:Rule(order = 2)
+ val networkCallbackRule = AutoReleaseNetworkCallbackRule()
+
companion object {
@JvmStatic @BeforeClass
fun setUpClass() {
@@ -144,15 +148,15 @@
assumeTrue(pm.hasSystemFeature(FEATURE_WIFI))
assumeFalse(pm.hasSystemFeature(FEATURE_WATCH))
utils.ensureWifiConnected()
- val cellNetwork = utils.connectToCell()
+ val cellNetwork = networkCallbackRule.requestCell()
// Verify cell network is validated
val cellReq = NetworkRequest.Builder()
.addTransportType(TRANSPORT_CELLULAR)
.addCapability(NET_CAPABILITY_INTERNET)
.build()
- val cellCb = TestableNetworkCallback(timeoutMs = TEST_TIMEOUT_MS)
- cm.registerNetworkCallback(cellReq, cellCb)
+ val cellCb = networkCallbackRule.registerNetworkCallback(cellReq,
+ TestableNetworkCallback(timeoutMs = TEST_TIMEOUT_MS))
val cb = cellCb.poll { it.network == cellNetwork &&
it is CapabilitiesChanged && it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
}
@@ -213,8 +217,6 @@
} finally {
cm.unregisterNetworkCallback(wifiCb)
server.stop()
- // disconnectFromCell should be called after connectToCell
- utils.disconnectFromCell()
}
}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 544f300..4d465ba 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -191,6 +191,7 @@
import com.android.networkstack.apishim.ConstantsShim;
import com.android.networkstack.apishim.NetworkInformationShimImpl;
import com.android.networkstack.apishim.common.ConnectivityManagerShim;
+import com.android.testutils.AutoReleaseNetworkCallbackRule;
import com.android.testutils.CompatUtil;
import com.android.testutils.ConnectivityModuleTest;
import com.android.testutils.DevSdkIgnoreRule;
@@ -259,10 +260,14 @@
@RunWith(AndroidJUnit4.class)
public class ConnectivityManagerTest {
- @Rule
+ @Rule(order = 1)
public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
- @Rule
+ @Rule(order = 2)
+ public final AutoReleaseNetworkCallbackRule
+ networkCallbackRule = new AutoReleaseNetworkCallbackRule();
+
+ @Rule(order = 3)
public final DeviceConfigRule mTestValidationConfigRule = new DeviceConfigRule(
5 /* retryCountBeforeSIfConfigChanged */);
@@ -411,11 +416,6 @@
@After
public void tearDown() throws Exception {
- // Release any NetworkRequests filed to connect mobile data.
- if (mCtsNetUtils.cellConnectAttempted()) {
- mCtsNetUtils.disconnectFromCell();
- }
-
if (TestUtils.shouldTestSApis()) {
runWithShellPermissionIdentity(
() -> mCmShim.setRequireVpnForUids(false, mVpnRequiredUidRanges),
@@ -555,7 +555,7 @@
throws InterruptedException {
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
// Make sure cell is active to retrieve IMSI for verification in later step.
- final Network cellNetwork = mCtsNetUtils.connectToCell();
+ final Network cellNetwork = networkCallbackRule.requestCell();
final String subscriberId = getSubscriberIdForCellNetwork(cellNetwork);
assertFalse(TextUtils.isEmpty(subscriberId));
@@ -802,7 +802,9 @@
assertNull(redactedNormal.getUids());
assertNull(redactedNormal.getSsid());
assertNull(redactedNormal.getUnderlyingNetworks());
- assertEquals(0, redactedNormal.getSubscriptionIds().size());
+ // TODO: Make subIds public and update to verify the size is 2
+ final int subIdsSize = redactedNormal.getSubscriptionIds().size();
+ assertTrue(subIdsSize == 0 || subIdsSize == 2);
assertEquals(WifiInfo.DEFAULT_MAC_ADDRESS,
((WifiInfo) redactedNormal.getTransportInfo()).getBSSID());
assertEquals(rssi, ((WifiInfo) redactedNormal.getTransportInfo()).getRssi());
@@ -851,7 +853,7 @@
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
- Network cellNetwork = mCtsNetUtils.connectToCell();
+ Network cellNetwork = networkCallbackRule.requestCell();
// This server returns the requestor's IP address as the response body.
URL url = new URL("http://google-ipv6test.appspot.com/ip.js?fmt=text");
String wifiAddressString = httpGet(wifiNetwork, url);
@@ -1353,9 +1355,7 @@
public void testToggleWifiConnectivityAction() throws Exception {
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
- // toggleWifi calls connectToWifi and disconnectFromWifi, which both wait for
- // CONNECTIVITY_ACTION broadcasts.
- mCtsNetUtils.toggleWifi();
+ mCtsNetUtils.reconnectWifiAndWaitForConnectivityAction();
}
/** Verify restricted networks cannot be requested. */
@@ -1554,6 +1554,40 @@
}
}
+ @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @ConnectivityModuleTest
+ public void testSetBackgroundNetworkingShellCommand() {
+ final int testUid = 54352;
+ runShellCommand("cmd connectivity set-background-networking-enabled-for-uid " + testUid
+ + " true");
+ int rule = runAsShell(NETWORK_SETTINGS,
+ () -> mCm.getUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, testUid));
+ assertEquals(rule, FIREWALL_RULE_ALLOW);
+
+ runShellCommand("cmd connectivity set-background-networking-enabled-for-uid " + testUid
+ + " false");
+ rule = runAsShell(NETWORK_SETTINGS,
+ () -> mCm.getUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, testUid));
+ assertEquals(rule, FIREWALL_RULE_DENY);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @ConnectivityModuleTest
+ public void testGetBackgroundNetworkingShellCommand() {
+ final int testUid = 54312;
+ runAsShell(NETWORK_SETTINGS,
+ () -> mCm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, testUid,
+ FIREWALL_RULE_ALLOW));
+ String output = runShellCommand(
+ "cmd connectivity get-background-networking-enabled-for-uid " + testUid);
+ assertTrue(output.contains("allow"));
+
+ runAsShell(NETWORK_SETTINGS,
+ () -> mCm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, testUid,
+ FIREWALL_RULE_DEFAULT));
+ output = runShellCommand(
+ "cmd connectivity get-background-networking-enabled-for-uid " + testUid);
+ assertTrue(output.contains("deny"));
+ }
+
// TODO: move the following socket keep alive test to dedicated test class.
/**
* Callback used in tcp keepalive offload that allows caller to wait callback fires.
@@ -1991,7 +2025,7 @@
return;
}
- final Network network = mCtsNetUtils.connectToCell();
+ final Network network = networkCallbackRule.requestCell();
final int supported = getSupportedKeepalivesForNet(network);
final InetAddress srcAddr = getFirstV4Address(network);
assumeTrue("This test requires native IPv4", srcAddr != null);
@@ -2166,8 +2200,7 @@
registerCallbackAndWaitForAvailable(makeWifiNetworkRequest(), wifiCb);
}
if (supportTelephony) {
- // connectToCell needs to be followed by disconnectFromCell, which is called in tearDown
- mCtsNetUtils.connectToCell();
+ networkCallbackRule.requestCell();
registerCallbackAndWaitForAvailable(makeCellNetworkRequest(), telephonyCb);
}
@@ -2958,7 +2991,7 @@
final TestableNetworkCallback wifiCb = new TestableNetworkCallback();
try {
// Ensure at least one default network candidate connected.
- mCtsNetUtils.connectToCell();
+ networkCallbackRule.requestCell();
final Network wifiNetwork = prepareUnvalidatedNetwork();
// Default network should not be wifi ,but checking that wifi is not the default doesn't
@@ -3000,7 +3033,7 @@
allowBadWifi();
try {
- final Network cellNetwork = mCtsNetUtils.connectToCell();
+ final Network cellNetwork = networkCallbackRule.requestCell();
final Network wifiNetwork = prepareValidatedNetwork();
registerDefaultNetworkCallback(defaultCb);
@@ -3180,8 +3213,6 @@
if (supportWifi) {
mCtsNetUtils.ensureWifiDisconnected(null /* wifiNetworkToCheck */);
- } else {
- mCtsNetUtils.disconnectFromCell();
}
final CompletableFuture<Boolean> future = new CompletableFuture<>();
@@ -3192,7 +3223,7 @@
if (supportWifi) {
mCtsNetUtils.ensureWifiConnected();
} else {
- mCtsNetUtils.connectToCell();
+ networkCallbackRule.requestCell();
}
assertTrue(future.get(LISTEN_ACTIVITY_TIMEOUT_MS, TimeUnit.MILLISECONDS));
}, () -> {
@@ -3233,7 +3264,7 @@
// For testing mobile data preferred uids feature, it needs both wifi and cell network.
final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
- final Network cellNetwork = mCtsNetUtils.connectToCell();
+ final Network cellNetwork = networkCallbackRule.requestCell();
final TestableNetworkCallback defaultTrackingCb = new TestableNetworkCallback();
final TestableNetworkCallback systemDefaultCb = new TestableNetworkCallback();
final Handler h = new Handler(Looper.getMainLooper());
diff --git a/tests/cts/net/src/android/net/cts/DiscoveryRequestTest.kt b/tests/cts/net/src/android/net/cts/DiscoveryRequestTest.kt
new file mode 100644
index 0000000..909a5bc
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DiscoveryRequestTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.cts
+
+import android.net.Network
+import android.net.nsd.DiscoveryRequest
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertParcelingIsLossless
+import com.android.testutils.assertThrows
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** CTS tests for {@link DiscoveryRequest}. */
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@ConnectivityModuleTest
+class DiscoveryRequestTest {
+ @Test
+ fun testParcelingIsLossLess() {
+ val requestWithNullFields =
+ DiscoveryRequest.Builder("_ipps._tcp").build()
+ val requestWithAllFields =
+ DiscoveryRequest.Builder("_ipps._tcp")
+ .setSubtype("_xyz")
+ .setNetwork(Network(1))
+ .build()
+
+ assertParcelingIsLossless(requestWithNullFields)
+ assertParcelingIsLossless(requestWithAllFields)
+ }
+
+ @Test
+ fun testBuilder_success() {
+ val request = DiscoveryRequest.Builder("_ipps._tcp")
+ .setSubtype("_xyz")
+ .setNetwork(Network(1))
+ .build()
+
+ assertEquals("_ipps._tcp", request.serviceType)
+ assertEquals("_xyz", request.subtype)
+ assertEquals(Network(1), request.network)
+ }
+
+ @Test
+ fun testBuilderConstructor_emptyServiceType_throwsIllegalArgument() {
+ assertThrows(IllegalArgumentException::class.java) {
+ DiscoveryRequest.Builder("")
+ }
+ }
+
+ @Test
+ fun testEquality() {
+ val request1 = DiscoveryRequest.Builder("_ipps._tcp").build()
+ val request2 = DiscoveryRequest.Builder("_ipps._tcp").build()
+ val request3 = DiscoveryRequest.Builder("_ipps._tcp")
+ .setSubtype("_xyz")
+ .setNetwork(Network(1))
+ .build()
+ val request4 = DiscoveryRequest.Builder("_ipps._tcp")
+ .setSubtype("_xyz")
+ .setNetwork(Network(1))
+ .build()
+
+ assertEquals(request1, request2)
+ assertEquals(request3, request4)
+ assertNotEquals(request1, request3)
+ assertNotEquals(request2, request4)
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index 9ff0f2f..752891f 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -23,6 +23,7 @@
import static android.net.DnsResolver.TYPE_AAAA;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import static android.system.OsConstants.ETIMEDOUT;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
@@ -59,11 +60,14 @@
import com.android.net.module.util.DnsPacket;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DeviceConfigRule;
import com.android.testutils.DnsResolverModuleTest;
import com.android.testutils.SkipPresubmit;
import org.junit.After;
import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -80,6 +84,8 @@
@AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
@RunWith(AndroidJUnit4.class)
public class DnsResolverTest {
+ @ClassRule
+ public static final DeviceConfigRule DEVICE_CONFIG_CLASS_RULE = new DeviceConfigRule();
@Rule
public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
@@ -123,6 +129,20 @@
private TestNetworkCallback mWifiRequestCallback = null;
+ /**
+ * @see BeforeClass
+ */
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ // Use async private DNS resolution to avoid flakes due to races applying the setting
+ DEVICE_CONFIG_CLASS_RULE.setConfig(NAMESPACE_CONNECTIVITY,
+ "networkmonitor_async_privdns_resolution", "1");
+ // Make sure NetworkMonitor is restarted before and after the test so the flag is applied
+ // and cleaned up.
+ maybeToggleWifiAndCell();
+ DEVICE_CONFIG_CLASS_RULE.runAfterNextCleanup(DnsResolverTest::maybeToggleWifiAndCell);
+ }
+
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getContext();
@@ -144,6 +164,12 @@
}
}
+ private static void maybeToggleWifiAndCell() throws Exception {
+ final CtsNetUtils utils = new CtsNetUtils(InstrumentationRegistry.getContext());
+ utils.reconnectWifiIfSupported();
+ utils.reconnectCellIfSupported();
+ }
+
private static String byteArrayToHexString(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; ++i) {
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index b7e5205..d052551 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -348,7 +348,9 @@
}
}
- private fun isEthernetSupported() = em != null
+ private fun isEthernetSupported() : Boolean {
+ return context.getSystemService(EthernetManager::class.java) != null
+ }
@Before
fun setUp() {
@@ -899,6 +901,20 @@
}
@Test
+ fun testEnableDisableInterface_callbacks() {
+ val iface = createInterface()
+ val listener = EthernetStateListener()
+ addInterfaceStateListener(listener)
+ listener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
+
+ disableInterface(iface).expectResult(iface.name)
+ listener.expectCallback(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+
+ enableInterface(iface).expectResult(iface.name)
+ listener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
+ }
+
+ @Test
fun testUpdateConfiguration_forBothIpConfigAndCapabilities() {
val iface = createInterface()
val cb = requestNetwork(ETH_REQUEST.copyWithEthernetSpecifier(iface.name))
diff --git a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
index 7f710d7..2a6c638 100644
--- a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
@@ -26,12 +26,15 @@
import static android.system.OsConstants.FIONREAD;
import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.IpSecAlgorithm;
import android.net.IpSecManager;
import android.net.IpSecTransform;
+import android.net.IpSecTransformState;
+import android.os.OutcomeReceiver;
import android.platform.test.annotations.AppModeFull;
import android.system.ErrnoException;
import android.system.Os;
@@ -65,8 +68,12 @@
import java.net.SocketImpl;
import java.net.SocketOptions;
import java.util.Arrays;
+import java.util.BitSet;
import java.util.HashSet;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@RunWith(AndroidJUnit4.class)
@@ -83,6 +90,7 @@
protected static final byte[] TEST_DATA = "Best test data ever!".getBytes();
protected static final int DATA_BUFFER_LEN = 4096;
protected static final int SOCK_TIMEOUT = 500;
+ protected static final int REPLAY_BITMAP_LEN_BYTE = 512;
private static final byte[] KEY_DATA = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
@@ -122,6 +130,47 @@
.getSystemService(Context.CONNECTIVITY_SERVICE);
}
+ protected static void checkTransformState(
+ IpSecTransform transform,
+ long txHighestSeqNum,
+ long rxHighestSeqNum,
+ long packetCnt,
+ long byteCnt,
+ byte[] replayBitmap)
+ throws Exception {
+ final CompletableFuture<IpSecTransformState> futureIpSecTransform =
+ new CompletableFuture<>();
+ transform.requestIpSecTransformState(
+ Executors.newSingleThreadExecutor(),
+ new OutcomeReceiver<IpSecTransformState, RuntimeException>() {
+ @Override
+ public void onResult(IpSecTransformState state) {
+ futureIpSecTransform.complete(state);
+ }
+ });
+
+ final IpSecTransformState transformState =
+ futureIpSecTransform.get(SOCK_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ assertEquals(txHighestSeqNum, transformState.getTxHighestSequenceNumber());
+ assertEquals(rxHighestSeqNum, transformState.getRxHighestSequenceNumber());
+ assertEquals(packetCnt, transformState.getPacketCount());
+ assertEquals(byteCnt, transformState.getByteCount());
+ assertArrayEquals(replayBitmap, transformState.getReplayBitmap());
+ }
+
+ protected static void checkTransformStateNoTraffic(IpSecTransform transform) throws Exception {
+ checkTransformState(transform, 0L, 0L, 0L, 0L, newReplayBitmap(0));
+ }
+
+ protected static byte[] newReplayBitmap(int receivedPktCnt) {
+ final BitSet bitSet = new BitSet(REPLAY_BITMAP_LEN_BYTE * 8);
+ for (int i = 0; i < receivedPktCnt; i++) {
+ bitSet.set(i);
+ }
+ return Arrays.copyOf(bitSet.toByteArray(), REPLAY_BITMAP_LEN_BYTE);
+ }
+
/** Checks if an IPsec algorithm is enabled on the device */
protected static boolean hasIpSecAlgorithm(String algorithm) {
if (SdkLevel.isAtLeastS()) {
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
index fe86a90..a40ed0f 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -63,11 +63,13 @@
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
+import android.net.InetAddresses;
import android.net.IpSecAlgorithm;
import android.net.IpSecManager;
import android.net.IpSecManager.SecurityParameterIndex;
import android.net.IpSecManager.UdpEncapsulationSocket;
import android.net.IpSecTransform;
+import android.net.NetworkUtils;
import android.net.TrafficStats;
import android.os.Build;
import android.platform.test.annotations.AppModeFull;
@@ -381,6 +383,22 @@
assumeTrue("Not supported by kernel", isIpv6UdpEncapSupportedByKernel());
}
+ // TODO: b/319532485 Figure out whether to support x86_32
+ private static boolean isRequestTransformStateSupportedByKernel() {
+ return NetworkUtils.isKernel64Bit() || !NetworkUtils.isKernelX86();
+ }
+
+ // Package private for use in IpSecManagerTunnelTest
+ static boolean isRequestTransformStateSupported() {
+ return SdkLevel.isAtLeastV() && isRequestTransformStateSupportedByKernel();
+ }
+
+ // Package private for use in IpSecManagerTunnelTest
+ static void assumeRequestIpSecTransformStateSupported() {
+ assumeTrue("Not supported before V", SdkLevel.isAtLeastV());
+ assumeTrue("Not supported by kernel", isRequestTransformStateSupportedByKernel());
+ }
+
@Test
public void testCreateTransformIpv4() throws Exception {
doTestCreateTransform(IPV4_LOOPBACK, false);
@@ -1596,4 +1614,32 @@
assertTrue("Returned invalid port", encapSocket.getPort() != 0);
}
}
+
+ @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ public void testRequestIpSecTransformState() throws Exception {
+ assumeRequestIpSecTransformStateSupported();
+
+ final InetAddress localAddr = InetAddresses.parseNumericAddress(IPV6_LOOPBACK);
+ try (SecurityParameterIndex spi = mISM.allocateSecurityParameterIndex(localAddr);
+ IpSecTransform transform =
+ buildTransportModeTransform(spi, localAddr, null /* encapSocket*/)) {
+ final SocketPair<JavaUdpSocket> sockets =
+ getJavaUdpSocketPair(localAddr, mISM, transform, false);
+
+ sockets.mLeftSock.sendTo(TEST_DATA, localAddr, sockets.mRightSock.getPort());
+ sockets.mRightSock.receive();
+
+ final int expectedPacketCount = 1;
+ final int expectedInnerPacketSize = TEST_DATA.length + UDP_HDRLEN;
+
+ checkTransformState(
+ transform,
+ expectedPacketCount,
+ expectedPacketCount,
+ 2 * (long) expectedPacketCount,
+ 2 * (long) expectedInnerPacketSize,
+ newReplayBitmap(expectedPacketCount));
+ }
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
index 1ede5c1..22a51d6 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
@@ -19,7 +19,9 @@
import static android.app.AppOpsManager.OP_MANAGE_IPSEC_TUNNELS;
import static android.net.IpSecManager.UdpEncapsulationSocket;
import static android.net.cts.IpSecManagerTest.assumeExperimentalIpv6UdpEncapSupported;
+import static android.net.cts.IpSecManagerTest.assumeRequestIpSecTransformStateSupported;
import static android.net.cts.IpSecManagerTest.isIpv6UdpEncapSupported;
+import static android.net.cts.IpSecManagerTest.isRequestTransformStateSupported;
import static android.net.cts.PacketUtils.AES_CBC_BLK_SIZE;
import static android.net.cts.PacketUtils.AES_CBC_IV_LEN;
import static android.net.cts.PacketUtils.BytePayload;
@@ -117,6 +119,8 @@
private static final int TIMEOUT_MS = 500;
+ private static final int PACKET_COUNT = 5000;
+
// Static state to reduce setup/teardown
private static ConnectivityManager sCM;
private static TestNetworkManager sTNM;
@@ -256,7 +260,7 @@
}
/* Test runnables for callbacks after IPsec tunnels are set up. */
- private abstract class IpSecTunnelTestRunnable {
+ private interface IpSecTunnelTestRunnable {
/**
* Runs the test code, and returns the inner socket port, if any.
*
@@ -282,8 +286,7 @@
throws Exception;
}
- private int getPacketSize(
- int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode) {
+ private static int getInnerPacketSize(int innerFamily, boolean transportInTunnelMode) {
int expectedPacketSize = TEST_DATA.length + UDP_HDRLEN;
// Inner Transport mode packet size
@@ -299,6 +302,13 @@
// Inner IP Header
expectedPacketSize += innerFamily == AF_INET ? IP4_HDRLEN : IP6_HDRLEN;
+ return expectedPacketSize;
+ }
+
+ private static int getPacketSize(
+ int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode) {
+ int expectedPacketSize = getInnerPacketSize(innerFamily, transportInTunnelMode);
+
// Tunnel mode transform size
expectedPacketSize =
PacketUtils.calculateEspPacketSize(
@@ -401,6 +411,20 @@
spi, TEST_DATA, useEncap, expectedPacketSize);
socket.close();
+ if (isRequestTransformStateSupported()) {
+ final int innerPacketSize =
+ getInnerPacketSize(innerFamily, transportInTunnelMode);
+
+ checkTransformState(
+ outTunnelTransform,
+ seqNum,
+ 0L,
+ seqNum,
+ seqNum * (long) innerPacketSize,
+ newReplayBitmap(0));
+ checkTransformStateNoTraffic(inTunnelTransform);
+ }
+
return innerSocketPort;
}
};
@@ -524,6 +548,22 @@
socket.close();
+ if (isRequestTransformStateSupported()) {
+ final int innerFamily =
+ localInner instanceof Inet4Address ? AF_INET : AF_INET6;
+ final int innerPacketSize =
+ getInnerPacketSize(innerFamily, transportInTunnelMode);
+
+ checkTransformStateNoTraffic(outTunnelTransform);
+ checkTransformState(
+ inTunnelTransform,
+ 0L,
+ seqNum,
+ seqNum,
+ seqNum * (long) innerPacketSize,
+ newReplayBitmap(seqNum));
+ }
+
return 0;
}
};
@@ -1127,6 +1167,18 @@
return innerSocketPort;
}
+ private int buildTunnelNetworkAndRunTestsSimple(int spi, IpSecTunnelTestRunnable test)
+ throws Exception {
+ return buildTunnelNetworkAndRunTests(
+ LOCAL_INNER_6,
+ REMOTE_INNER_6,
+ LOCAL_OUTER_6,
+ REMOTE_OUTER_6,
+ spi,
+ null /* encapSocket */,
+ test);
+ }
+
private static void receiveAndValidatePacket(JavaUdpSocket socket) throws Exception {
byte[] socketResponseBytes = socket.receive();
assertArrayEquals(TEST_DATA, socketResponseBytes);
@@ -1691,4 +1743,101 @@
assumeExperimentalIpv6UdpEncapSupported();
doTestMigrateTunnelModeTransform(AF_INET6, AF_INET6, true, false);
}
+
+ @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ public void testRequestIpSecTransformStateForRx() throws Exception {
+ assumeRequestIpSecTransformStateSupported();
+
+ final int spi = getRandomSpi(LOCAL_OUTER_6, REMOTE_OUTER_6);
+ buildTunnelNetworkAndRunTestsSimple(
+ spi,
+ (ipsecNetwork,
+ tunnelIface,
+ tunUtils,
+ inTunnelTransform,
+ outTunnelTransform,
+ localOuter,
+ remoteOuter,
+ seqNum) -> {
+ // Build a socket and send traffic
+ final JavaUdpSocket socket = new JavaUdpSocket(LOCAL_INNER_6);
+ ipsecNetwork.bindSocket(socket.mSocket);
+ int innerSocketPort = socket.getPort();
+
+ for (int i = 1; i < PACKET_COUNT + 1; i++) {
+ byte[] pkt =
+ getTunnelModePacket(
+ spi,
+ REMOTE_INNER_6,
+ LOCAL_INNER_6,
+ remoteOuter,
+ localOuter,
+ innerSocketPort,
+ 0,
+ i);
+ tunUtils.injectPacket(pkt);
+ receiveAndValidatePacket(socket);
+ }
+
+ final int innerPacketSize = getInnerPacketSize(AF_INET6, false);
+ checkTransformState(
+ inTunnelTransform,
+ 0L,
+ PACKET_COUNT,
+ PACKET_COUNT,
+ PACKET_COUNT * (long) innerPacketSize,
+ newReplayBitmap(REPLAY_BITMAP_LEN_BYTE * 8));
+
+ return innerSocketPort;
+ });
+ }
+
+ @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ public void testRequestIpSecTransformStateForTx() throws Exception {
+ assumeRequestIpSecTransformStateSupported();
+
+ final int spi = getRandomSpi(LOCAL_OUTER_6, REMOTE_OUTER_6);
+ buildTunnelNetworkAndRunTestsSimple(
+ spi,
+ (ipsecNetwork,
+ tunnelIface,
+ tunUtils,
+ inTunnelTransform,
+ outTunnelTransform,
+ localOuter,
+ remoteOuter,
+ seqNum) -> {
+ // Build a socket and send traffic
+ final JavaUdpSocket outSocket = new JavaUdpSocket(LOCAL_INNER_6);
+ ipsecNetwork.bindSocket(outSocket.mSocket);
+ int innerSocketPort = outSocket.getPort();
+
+ int expectedPacketSize =
+ getPacketSize(
+ AF_INET6,
+ AF_INET6,
+ false /* useEncap */,
+ false /* transportInTunnelMode */);
+
+ for (int i = 0; i < PACKET_COUNT; i++) {
+ outSocket.sendTo(TEST_DATA, REMOTE_INNER_6, innerSocketPort);
+ tunUtils.awaitEspPacketNoPlaintext(
+ spi, TEST_DATA, false /* useEncap */, expectedPacketSize);
+ }
+
+ final int innerPacketSize =
+ getInnerPacketSize(AF_INET6, false /* transportInTunnelMode */);
+ checkTransformState(
+ outTunnelTransform,
+ PACKET_COUNT,
+ 0L,
+ PACKET_COUNT,
+ PACKET_COUNT * (long) innerPacketSize,
+ newReplayBitmap(0));
+
+ return innerSocketPort;
+ });
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/IpSecTransformStateTest.java b/tests/cts/net/src/android/net/cts/IpSecTransformStateTest.java
new file mode 100644
index 0000000..7b42306
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IpSecTransformStateTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import android.net.IpSecTransformState;
+import android.os.Build;
+import android.os.SystemClock;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@RunWith(DevSdkIgnoreRunner.class)
+public class IpSecTransformStateTest {
+ @Rule public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+ private static final long TIMESTAMP_MILLIS = 1000L;
+ private static final long HIGHEST_SEQ_NUMBER_TX = 10000L;
+ private static final long HIGHEST_SEQ_NUMBER_RX = 20000L;
+ private static final long PACKET_COUNT = 9000L;
+ private static final long BYTE_COUNT = 900000L;
+
+ private static final int REPLAY_BITMAP_LEN_BYTE = 512;
+ private static final byte[] REPLAY_BITMAP_NO_PACKETS = new byte[REPLAY_BITMAP_LEN_BYTE];
+ private static final byte[] REPLAY_BITMAP_ALL_RECEIVED = new byte[REPLAY_BITMAP_LEN_BYTE];
+
+ static {
+ for (int i = 0; i < REPLAY_BITMAP_ALL_RECEIVED.length; i++) {
+ REPLAY_BITMAP_ALL_RECEIVED[i] = (byte) 0xff;
+ }
+ }
+
+ @Test
+ public void testBuildAndGet() {
+ final IpSecTransformState state =
+ new IpSecTransformState.Builder()
+ .setTimestampMillis(TIMESTAMP_MILLIS)
+ .setTxHighestSequenceNumber(HIGHEST_SEQ_NUMBER_TX)
+ .setRxHighestSequenceNumber(HIGHEST_SEQ_NUMBER_RX)
+ .setPacketCount(PACKET_COUNT)
+ .setByteCount(BYTE_COUNT)
+ .setReplayBitmap(REPLAY_BITMAP_ALL_RECEIVED)
+ .build();
+
+ assertEquals(TIMESTAMP_MILLIS, state.getTimestampMillis());
+ assertEquals(HIGHEST_SEQ_NUMBER_TX, state.getTxHighestSequenceNumber());
+ assertEquals(HIGHEST_SEQ_NUMBER_RX, state.getRxHighestSequenceNumber());
+ assertEquals(PACKET_COUNT, state.getPacketCount());
+ assertEquals(BYTE_COUNT, state.getByteCount());
+ assertArrayEquals(REPLAY_BITMAP_ALL_RECEIVED, state.getReplayBitmap());
+ }
+
+ @Test
+ public void testSelfGeneratedTimestampMillis() {
+ final long elapsedRealtimeBefore = SystemClock.elapsedRealtime();
+
+ final IpSecTransformState state =
+ new IpSecTransformState.Builder().setReplayBitmap(REPLAY_BITMAP_NO_PACKETS).build();
+
+ final long elapsedRealtimeAfter = SystemClock.elapsedRealtime();
+
+ // Verify elapsedRealtimeBefore <= state.getTimestampMillis() <= elapsedRealtimeAfter
+ assertFalse(elapsedRealtimeBefore > state.getTimestampMillis());
+ assertFalse(elapsedRealtimeAfter < state.getTimestampMillis());
+ }
+
+ @Test
+ public void testBuildWithoutReplayBitmap() throws Exception {
+ try {
+ new IpSecTransformState.Builder().build();
+ fail("Expected expcetion if replay bitmap is not set");
+ } catch (NullPointerException expected) {
+ }
+ }
+}
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/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
index 17a9ca2..73f65e0 100644
--- a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
+++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
@@ -16,7 +16,17 @@
package android.net.cts;
+import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
+import static android.content.pm.PackageManager.FEATURE_WIFI;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
import android.content.ContentResolver;
import android.content.Context;
@@ -28,9 +38,29 @@
import android.platform.test.annotations.AppModeFull;
import android.system.ErrnoException;
import android.system.OsConstants;
-import android.test.AndroidTestCase;
+import android.util.ArraySet;
-public class MultinetworkApiTest extends AndroidTestCase {
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.testutils.AutoReleaseNetworkCallbackRule;
+import com.android.testutils.DeviceConfigRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class MultinetworkApiTest {
+ @Rule(order = 1)
+ public final DeviceConfigRule mDeviceConfigRule = new DeviceConfigRule();
+
+ @Rule(order = 2)
+ public final AutoReleaseNetworkCallbackRule
+ mNetworkCallbackRule = new AutoReleaseNetworkCallbackRule();
static {
System.loadLibrary("nativemultinetwork_jni");
@@ -56,24 +86,20 @@
private ContentResolver mCR;
private ConnectivityManager mCM;
private CtsNetUtils mCtsNetUtils;
- private String mOldMode;
- private String mOldDnsSpecifier;
+ private Context mContext;
+ private Network mRequestedCellNetwork;
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- mCM = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
- mCR = getContext().getContentResolver();
- mCtsNetUtils = new CtsNetUtils(getContext());
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mCM = mContext.getSystemService(ConnectivityManager.class);
+ mCR = mContext.getContentResolver();
+ mCtsNetUtils = new CtsNetUtils(mContext);
}
- @Override
- protected void tearDown() throws Exception {
- super.tearDown();
- }
-
- public void testGetaddrinfo() throws ErrnoException {
- for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ @Test
+ public void testGetaddrinfo() throws Exception {
+ for (Network network : getTestableNetworks()) {
int errno = runGetaddrinfoCheck(network.getNetworkHandle());
if (errno != 0) {
throw new ErrnoException(
@@ -82,13 +108,14 @@
}
}
+ @Test
@AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
- public void testSetprocnetwork() throws ErrnoException {
+ public void testSetprocnetwork() throws Exception {
// Hopefully no prior test in this process space has set a default network.
assertNull(mCM.getProcessDefaultNetwork());
assertEquals(0, NetworkUtils.getBoundNetworkForProcess());
- for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ for (Network network : getTestableNetworks()) {
mCM.setProcessDefaultNetwork(null);
assertNull(mCM.getProcessDefaultNetwork());
@@ -107,7 +134,7 @@
mCM.setProcessDefaultNetwork(null);
}
- for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ for (Network network : getTestableNetworks()) {
NetworkUtils.bindProcessToNetwork(0);
assertNull(mCM.getBoundNetworkForProcess());
@@ -125,9 +152,10 @@
}
}
+ @Test
@AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
- public void testSetsocknetwork() throws ErrnoException {
- for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ public void testSetsocknetwork() throws Exception {
+ for (Network network : getTestableNetworks()) {
int errno = runSetsocknetwork(network.getNetworkHandle());
if (errno != 0) {
throw new ErrnoException(
@@ -136,8 +164,9 @@
}
}
- public void testNativeDatagramTransmission() throws ErrnoException {
- for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ @Test
+ public void testNativeDatagramTransmission() throws Exception {
+ for (Network network : getTestableNetworks()) {
int errno = runDatagramCheck(network.getNetworkHandle());
if (errno != 0) {
throw new ErrnoException(
@@ -146,7 +175,8 @@
}
}
- public void testNoSuchNetwork() {
+ @Test
+ public void testNoSuchNetwork() throws Exception {
final Network eNoNet = new Network(54321);
assertNull(mCM.getNetworkInfo(eNoNet));
@@ -158,9 +188,10 @@
// assertEquals(-OsConstants.ENONET, runGetaddrinfoCheck(eNoNetHandle));
}
- public void testNetworkHandle() {
+ @Test
+ public void testNetworkHandle() throws Exception {
// Test Network -> NetworkHandle -> Network results in the same Network.
- for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ for (Network network : getTestableNetworks()) {
long networkHandle = network.getNetworkHandle();
Network newNetwork = Network.fromNetworkHandle(networkHandle);
assertEquals(newNetwork, network);
@@ -181,10 +212,9 @@
} catch (IllegalArgumentException e) {}
}
+ @Test
public void testResNApi() throws Exception {
- final Network[] testNetworks = mCtsNetUtils.getTestableNetworks();
-
- for (Network network : testNetworks) {
+ for (Network network : getTestableNetworks()) {
// Throws AssertionError directly in jni function if test fail.
runResNqueryCheck(network.getNetworkHandle());
runResNsendCheck(network.getNetworkHandle());
@@ -201,14 +231,26 @@
}
}
+ @Test
@AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
- public void testResNApiNXDomainPrivateDns() throws InterruptedException {
+ public void testResNApiNXDomainPrivateDns() throws Exception {
+ // Use async private DNS resolution to avoid flakes due to races applying the setting
+ mDeviceConfigRule.setConfig(NAMESPACE_CONNECTIVITY,
+ "networkmonitor_async_privdns_resolution", "1");
+ mCtsNetUtils.reconnectWifiIfSupported();
+ mCtsNetUtils.reconnectCellIfSupported();
+
mCtsNetUtils.storePrivateDnsSetting();
+
+ mDeviceConfigRule.runAfterNextCleanup(() -> {
+ mCtsNetUtils.reconnectWifiIfSupported();
+ mCtsNetUtils.reconnectCellIfSupported();
+ });
// Enable private DNS strict mode and set server to dns.google before doing NxDomain test.
// b/144521720
try {
mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
- for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ for (Network network : getTestableNetworks()) {
// Wait for private DNS setting to propagate.
mCtsNetUtils.awaitPrivateDnsSetting("NxDomain test wait private DNS setting timeout",
network, GOOGLE_PRIVATE_DNS_SERVER, true);
@@ -218,4 +260,44 @@
mCtsNetUtils.restorePrivateDnsSetting();
}
}
+
+ /**
+ * Get all testable Networks with internet capability.
+ */
+ private Set<Network> getTestableNetworks() throws InterruptedException {
+ // Obtain cell and Wi-Fi through CtsNetUtils (which uses NetworkCallbacks), as they may have
+ // just been reconnected by the test using NetworkCallbacks, so synchronous calls may not
+ // yet return them (synchronous calls and callbacks should not be mixed for a given
+ // Network).
+ final Set<Network> testableNetworks = new ArraySet<>();
+ if (mContext.getPackageManager().hasSystemFeature(FEATURE_TELEPHONY)) {
+ if (mRequestedCellNetwork == null) {
+ mRequestedCellNetwork = mNetworkCallbackRule.requestCell();
+ }
+ assertNotNull("Cell network requested but not obtained", mRequestedCellNetwork);
+ testableNetworks.add(mRequestedCellNetwork);
+ }
+
+ if (mContext.getPackageManager().hasSystemFeature(FEATURE_WIFI)) {
+ testableNetworks.add(mCtsNetUtils.ensureWifiConnected());
+ }
+
+ // Obtain other networks through the synchronous API, if any.
+ for (Network network : mCtsNetUtils.getTestableNetworks()) {
+ final NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
+ if (nc != null
+ && !nc.hasTransport(TRANSPORT_WIFI)
+ && !nc.hasTransport(TRANSPORT_CELLULAR)) {
+ testableNetworks.add(network);
+ }
+ }
+
+ // In practice this should not happen as getTestableNetworks throws if there is no network
+ // at all.
+ assertFalse("This device does not support WiFi nor cell data, and does not have any other "
+ + "network connected. This test requires at least one internet-providing "
+ + "network.",
+ testableNetworks.isEmpty());
+ return testableNetworks;
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 84b6745..beb9274 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -21,6 +21,7 @@
import android.app.Instrumentation
import android.content.Context
import android.content.pm.PackageManager
+import android.content.pm.PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION
import android.net.ConnectivityManager
import android.net.EthernetNetworkSpecifier
import android.net.INetworkAgent
@@ -70,6 +71,7 @@
import android.net.TelephonyNetworkSpecifier
import android.net.TestNetworkInterface
import android.net.TestNetworkManager
+import android.net.TransportInfo
import android.net.Uri
import android.net.VpnManager
import android.net.VpnTransportInfo
@@ -150,6 +152,7 @@
import kotlin.test.assertTrue
import kotlin.test.fail
import org.junit.After
+import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -574,27 +577,13 @@
}
private fun doTestAllowedUids(
- subId: Int,
- transport: Int,
- uid: Int,
- expectUidsPresent: Boolean
- ) {
- doTestAllowedUids(subId, intArrayOf(transport), uid, expectUidsPresent)
- }
-
- private fun doTestAllowedUids(
- subId: Int,
transports: IntArray,
uid: Int,
- expectUidsPresent: Boolean
+ expectUidsPresent: Boolean,
+ specifier: NetworkSpecifier?,
+ transportInfo: TransportInfo?
) {
val callback = TestableNetworkCallback(DEFAULT_TIMEOUT_MS)
- val specifier = when {
- transports.size != 1 -> null
- TRANSPORT_ETHERNET in transports -> EthernetNetworkSpecifier("testInterface")
- TRANSPORT_CELLULAR in transports -> TelephonyNetworkSpecifier(subId)
- else -> null
- }
val agent = createNetworkAgent(initialNc = NetworkCapabilities.Builder().run {
addTransportType(TRANSPORT_TEST)
transports.forEach { addTransportType(it) }
@@ -602,10 +591,7 @@
addCapability(NET_CAPABILITY_NOT_SUSPENDED)
removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
setNetworkSpecifier(specifier)
- if (TRANSPORT_WIFI in transports && SdkLevel.isAtLeastV()) {
- // setSubscriptionId only exists in V+
- setTransportInfo(WifiInfo.Builder().setSubscriptionId(subId).build())
- }
+ setTransportInfo(transportInfo)
setAllowedUids(setOf(uid))
setOwnerUid(Process.myUid())
setAdministratorUids(intArrayOf(Process.myUid()))
@@ -630,6 +616,45 @@
// callback will be unregistered in tearDown()
}
+ private fun doTestAllowedUids(
+ transport: Int,
+ uid: Int,
+ expectUidsPresent: Boolean
+ ) {
+ doTestAllowedUids(intArrayOf(transport), uid, expectUidsPresent,
+ specifier = null, transportInfo = null)
+ }
+
+ private fun doTestAllowedUidsWithSubId(
+ subId: Int,
+ transport: Int,
+ uid: Int,
+ expectUidsPresent: Boolean
+ ) {
+ doTestAllowedUidsWithSubId(subId, intArrayOf(transport), uid, expectUidsPresent)
+ }
+
+ private fun doTestAllowedUidsWithSubId(
+ subId: Int,
+ transports: IntArray,
+ uid: Int,
+ expectUidsPresent: Boolean
+ ) {
+ val specifier = when {
+ transports.size != 1 -> null
+ TRANSPORT_ETHERNET in transports -> EthernetNetworkSpecifier("testInterface")
+ TRANSPORT_CELLULAR in transports -> TelephonyNetworkSpecifier(subId)
+ else -> null
+ }
+ val transportInfo = if (TRANSPORT_WIFI in transports && SdkLevel.isAtLeastV()) {
+ // setSubscriptionId only exists in V+
+ WifiInfo.Builder().setSubscriptionId(subId).build()
+ } else {
+ null
+ }
+ doTestAllowedUids(transports, uid, expectUidsPresent, specifier, transportInfo)
+ }
+
private fun setHoldCarrierPrivilege(hold: Boolean, subId: Int) {
fun getCertHash(): String {
val pkgInfo = realContext.packageManager.getPackageInfo(realContext.opPackageName,
@@ -723,6 +748,19 @@
@Test
@IgnoreUpTo(Build.VERSION_CODES.S)
fun testAllowedUids() {
+ doTestAllowedUids(TRANSPORT_CELLULAR, Process.myUid(), expectUidsPresent = false)
+ doTestAllowedUids(TRANSPORT_WIFI, Process.myUid(), expectUidsPresent = false)
+ doTestAllowedUids(TRANSPORT_BLUETOOTH, Process.myUid(), expectUidsPresent = false)
+
+ // TODO(b/315136340): Allow ownerUid to see allowedUids and add cases that expect uids
+ // present
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S)
+ fun testAllowedUids_WithCarrierServicePackage() {
+ assumeTrue(realContext.packageManager.hasSystemFeature(FEATURE_TELEPHONY_SUBSCRIPTION))
+
// Use a different package than this one to make sure that a package that doesn't hold
// carrier service permission can be set as an allowed UID.
val servicePackage = "android.net.cts.carrierservicepackage"
@@ -735,12 +773,17 @@
val tm = realContext.getSystemService(TelephonyManager::class.java)!!
val defaultSubId = SubscriptionManager.getDefaultSubscriptionId()
+ assertTrue(defaultSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID,
+ "getDefaultSubscriptionId returns INVALID_SUBSCRIPTION_ID")
tryTest {
// This process is not the carrier service UID, so allowedUids should be ignored in all
// the following cases.
- doTestAllowedUids(defaultSubId, TRANSPORT_CELLULAR, uid, expectUidsPresent = false)
- doTestAllowedUids(defaultSubId, TRANSPORT_WIFI, uid, expectUidsPresent = false)
- doTestAllowedUids(defaultSubId, TRANSPORT_BLUETOOTH, uid, expectUidsPresent = false)
+ doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_CELLULAR, uid,
+ expectUidsPresent = false)
+ doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_WIFI, uid,
+ expectUidsPresent = false)
+ doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_BLUETOOTH, uid,
+ expectUidsPresent = false)
// The tools to set the carrier service package override do not exist before U,
// so there is no way to test the rest of this test on < U.
@@ -783,9 +826,10 @@
// TODO(b/315136340): Allow ownerUid to see allowedUids and enable below test case
// doTestAllowedUids(defaultSubId, TRANSPORT_WIFI, uid, expectUidsPresent = true)
}
- doTestAllowedUids(defaultSubId, TRANSPORT_BLUETOOTH, uid, expectUidsPresent = false)
- doTestAllowedUids(defaultSubId, intArrayOf(TRANSPORT_CELLULAR, TRANSPORT_WIFI), uid,
+ doTestAllowedUidsWithSubId(defaultSubId, TRANSPORT_BLUETOOTH, uid,
expectUidsPresent = false)
+ doTestAllowedUidsWithSubId(defaultSubId, intArrayOf(TRANSPORT_CELLULAR, TRANSPORT_WIFI),
+ uid, expectUidsPresent = false)
} cleanupStep {
if (SdkLevel.isAtLeastU()) setCarrierServicePackageOverride(defaultSubId, null)
} cleanup {
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 594f3fb..6ec4e62 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -32,6 +32,8 @@
import static com.android.testutils.DevSdkIgnoreRuleKt.VANILLA_ICE_CREAM;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertArrayEquals;
@@ -62,6 +64,7 @@
import com.android.networkstack.apishim.NetworkRequestShimImpl;
import com.android.networkstack.apishim.common.NetworkRequestShim;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.testutils.ConnectivityModuleTest;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -72,6 +75,7 @@
import java.util.Set;
@RunWith(AndroidJUnit4.class)
+@ConnectivityModuleTest
public class NetworkRequestTest {
@Rule
public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
@@ -173,6 +177,20 @@
}
@Test
+ @IgnoreUpTo(Build.VERSION_CODES.S)
+ public void testSubscriptionIds() {
+ int[] subIds = {1, 2};
+ assertTrue(
+ new NetworkRequest.Builder().build()
+ .getSubscriptionIds().isEmpty());
+ assertThat(new NetworkRequest.Builder()
+ .setSubscriptionIds(Set.of(subIds[0], subIds[1]))
+ .build()
+ .getSubscriptionIds())
+ .containsExactly(subIds[0], subIds[1]);
+ }
+
+ @Test
@IgnoreUpTo(Build.VERSION_CODES.Q)
public void testRequestorPackageName() {
assertNull(new NetworkRequest.Builder().build().getRequestorPackageName());
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index 6a019b7..2315940 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -805,7 +805,7 @@
// harness, which is untagged, won't cause a failure.
long firstTotal = resultsWithTraffic.get(0).total;
for (QueryResult queryResult : resultsWithTraffic) {
- assertWithinPercentage(queryResult + "", firstTotal, queryResult.total, 12);
+ assertWithinPercentage(queryResult + "", firstTotal, queryResult.total, 16);
}
// Expect to see no traffic when querying for any tag in tagsWithNoTraffic or any
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index a040201..6dd4857 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -53,6 +53,7 @@
import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost
import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
import android.net.cts.util.CtsNetUtils
+import android.net.nsd.DiscoveryRequest
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.net.nsd.OffloadEngine
@@ -61,6 +62,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,21 +71,25 @@
import android.system.OsConstants.ETH_P_IPV6
import android.system.OsConstants.IPPROTO_IPV6
import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.RT_SCOPE_LINK
import android.system.OsConstants.SOCK_DGRAM
import android.util.Log
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.compatibility.common.util.PollingCheck
import com.android.compatibility.common.util.PropertyUtil
+import com.android.compatibility.common.util.SystemUtil
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 +139,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 +151,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")
@@ -152,7 +162,11 @@
private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
private val serviceName = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
private val serviceName2 = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
+ private val serviceName3 = "NsdTest%09d".format(Random().nextInt(1_000_000_000))
private val serviceType = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
+ private val serviceType2 = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
+ private val customHostname = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
+ private val customHostname2 = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
private val handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName)
private val ctsNetUtils by lazy{ CtsNetUtils(context) }
@@ -671,6 +685,48 @@
}
}
+ @Test
+ fun testRegisterService_twoServicesWithSameNameButDifferentTypes_registeredAndDiscoverable() {
+ val si1 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceName = serviceName
+ it.serviceType = serviceType
+ it.port = TEST_PORT
+ }
+ val si2 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceName = serviceName
+ it.serviceType = serviceType2
+ it.port = TEST_PORT + 1
+ }
+ val registrationRecord1 = NsdRegistrationRecord()
+ val registrationRecord2 = NsdRegistrationRecord()
+ val discoveryRecord1 = NsdDiscoveryRecord()
+ val discoveryRecord2 = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord1, si1)
+ registerService(registrationRecord2, si2)
+
+ nsdManager.discoverServices(serviceType,
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord1)
+ nsdManager.discoverServices(serviceType2,
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord2)
+
+ discoveryRecord1.waitForServiceDiscovered(serviceName, serviceType,
+ testNetwork1.network)
+ discoveryRecord2.waitForServiceDiscovered(serviceName, serviceType2,
+ testNetwork1.network)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord1)
+ nsdManager.stopServiceDiscovery(discoveryRecord2)
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord1)
+ nsdManager.unregisterService(registrationRecord2)
+ }
+ }
+
fun checkOffloadServiceInfo(serviceInfo: OffloadServiceInfo, si: NsdServiceInfo) {
val expectedServiceType = si.serviceType.split(",")[0]
assertEquals(si.serviceName, serviceInfo.key.serviceName)
@@ -678,11 +734,12 @@
assertEquals(listOf("_subtype"), serviceInfo.subtypes)
assertTrue(serviceInfo.hostname.startsWith("Android_"))
assertTrue(serviceInfo.hostname.endsWith("local"))
- assertEquals(0, serviceInfo.priority)
+ // Test service types should not be in the priority list
+ assertEquals(Integer.MAX_VALUE, serviceInfo.priority)
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)
@@ -1033,9 +1090,11 @@
nsdManager.discoverServices("_subtype1.$serviceType",
NsdManager.PROTOCOL_DNS_SD,
testNetwork1.network, Executor { it.run() }, subtype1DiscoveryRecord)
- nsdManager.discoverServices("_subtype2.$serviceType",
- NsdManager.PROTOCOL_DNS_SD,
- testNetwork1.network, Executor { it.run() }, subtype2DiscoveryRecord)
+
+ nsdManager.discoverServices(
+ DiscoveryRequest.Builder(serviceType).setSubtype("_subtype2")
+ .setNetwork(testNetwork1.network).build(),
+ Executor { it.run() }, subtype2DiscoveryRecord)
val info1 = subtype1DiscoveryRecord.waitForServiceDiscovered(
serviceName, serviceType, testNetwork1.network)
@@ -1090,6 +1149,73 @@
}
@Test
+ fun testSubtypeAdvertisingAndDiscovery_nonAlphanumericalSubtypes() {
+ // All non-alphanumerical characters between 0x20 and 0x7e, with a leading underscore
+ val nonAlphanumSubtype = "_ !\"#\$%&'()*+-/:;<=>?@[\\]^_`{|}"
+ // Test both legacy syntax and the subtypes setter, on different networks
+ val si1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType = "$serviceType,_test1,$nonAlphanumSubtype"
+ }
+ val si2 = makeTestServiceInfo(network = testNetwork2.network).apply {
+ subtypes = setOf("_test2", nonAlphanumSubtype)
+ }
+
+ val registrationRecord1 = NsdRegistrationRecord()
+ val registrationRecord2 = NsdRegistrationRecord()
+ val subtypeDiscoveryRecord1 = NsdDiscoveryRecord()
+ val subtypeDiscoveryRecord2 = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord1, si1)
+ registerService(registrationRecord2, si2)
+ nsdManager.discoverServices(DiscoveryRequest.Builder(serviceType)
+ .setSubtype(nonAlphanumSubtype)
+ .setNetwork(testNetwork1.network)
+ .build(), { it.run() }, subtypeDiscoveryRecord1)
+ nsdManager.discoverServices("$nonAlphanumSubtype.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD, testNetwork2.network, { it.run() },
+ subtypeDiscoveryRecord2)
+
+ val discoveredInfo1 = subtypeDiscoveryRecord1.waitForServiceDiscovered(serviceName,
+ serviceType, testNetwork1.network)
+ val discoveredInfo2 = subtypeDiscoveryRecord2.waitForServiceDiscovered(serviceName,
+ serviceType, testNetwork2.network)
+ assertTrue(discoveredInfo1.subtypes.contains(nonAlphanumSubtype))
+ assertTrue(discoveredInfo2.subtypes.contains(nonAlphanumSubtype))
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(subtypeDiscoveryRecord1)
+ subtypeDiscoveryRecord1.expectCallback<DiscoveryStopped>()
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(subtypeDiscoveryRecord2)
+ subtypeDiscoveryRecord2.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord1)
+ nsdManager.unregisterService(registrationRecord2)
+ }
+ }
+
+ @Test
+ fun testSubtypeDiscovery_typeMatchButSubtypeNotMatch_notDiscovered() {
+ val si1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType += ",_subtype1"
+ }
+ val registrationRecord = NsdRegistrationRecord()
+ val subtype2DiscoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord, si1)
+ val request = DiscoveryRequest.Builder(serviceType)
+ .setSubtype("_subtype2").setNetwork(testNetwork1.network).build()
+ nsdManager.discoverServices(request, { it.run() }, subtype2DiscoveryRecord)
+ subtype2DiscoveryRecord.expectCallback<DiscoveryStarted>()
+ subtype2DiscoveryRecord.assertNoCallback(timeoutMs = 2000)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(subtype2DiscoveryRecord)
+ subtype2DiscoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord)
+ }
+ }
+
+ @Test
fun testSubtypeAdvertising_tooManySubtypes_returnsFailureBadParameters() {
val si = makeTestServiceInfo(network = testNetwork1.network)
// Sets 101 subtypes in total
@@ -1154,6 +1280,83 @@
}
@Test
+ fun testRegisterServiceWithCustomHostAndAddresses_conflictDuringProbing_hostRenamed() {
+ val si = makeTestServiceInfo(testNetwork1.network).apply {
+ hostname = customHostname
+ hostAddresses = listOf(
+ parseNumericAddress("192.0.2.24"),
+ parseNumericAddress("2001:db8::3"))
+ }
+
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+ handlerThread.waitForIdle(TIMEOUT_MS)
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
+ registrationRecord)
+
+ tryTest {
+ assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+ "Did not find a probe for the service")
+ packetReader.sendResponse(buildConflictingAnnouncementForCustomHost())
+
+ // Registration must use an updated hostname to avoid the conflict
+ val cb = registrationRecord.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
+ // Service name is not renamed because there's no conflict on the service name.
+ assertEquals(serviceName, cb.serviceInfo.serviceName)
+ val hostname = cb.serviceInfo.hostname ?: fail("Missing hostname")
+ hostname.let {
+ assertTrue("Unexpected registered hostname: $it",
+ it.startsWith(customHostname) && it != customHostname)
+ }
+ } cleanupStep {
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ }
+ }
+
+ @Test
+ fun testRegisterServiceWithCustomHostNoAddresses_noConflictDuringProbing_notRenamed() {
+ val si = makeTestServiceInfo(testNetwork1.network).apply {
+ hostname = customHostname
+ }
+
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+ handlerThread.waitForIdle(TIMEOUT_MS)
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, { it.run() },
+ registrationRecord)
+
+ tryTest {
+ assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+ "Did not find a probe for the service")
+ // Not a conflict because no record is registered for the hostname
+ packetReader.sendResponse(buildConflictingAnnouncementForCustomHost())
+
+ // Registration is not renamed because there's no conflict
+ val cb = registrationRecord.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
+ assertEquals(serviceName, cb.serviceInfo.serviceName)
+ assertEquals(customHostname, cb.serviceInfo.hostname)
+ } cleanupStep {
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ }
+ }
+
+ @Test
fun testRegisterWithConflictAfterProbing() {
// This test requires shims supporting T+ APIs (NsdServiceInfo.network)
assumeTrue(TestUtils.shouldTestTApis())
@@ -1228,6 +1431,121 @@
}
}
+ @Test
+ fun testRegisterServiceWithCustomHostAndAddresses_conflictAfterProbing_hostRenamed() {
+ val si = makeTestServiceInfo(testNetwork1.network).apply {
+ hostname = customHostname
+ hostAddresses = listOf(
+ parseNumericAddress("192.0.2.24"),
+ parseNumericAddress("2001:db8::3"))
+ }
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ val discoveryRecord = NsdDiscoveryRecord()
+ val registeredService = registerService(registrationRecord, si)
+ val packetReader = TapPacketReader(
+ Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+ handlerThread.waitForIdle(TIMEOUT_MS)
+
+ tryTest {
+ repeat(3) {
+ assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
+ "Expect 3 announcements sent after initial probing")
+ }
+
+ assertEquals(si.serviceName, registeredService.serviceName)
+ assertEquals(si.hostname, registeredService.hostname)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, { it.run() }, discoveryRecord)
+ val discoveredInfo = discoveryRecord.waitForServiceDiscovered(
+ si.serviceName, serviceType)
+
+ // Send a conflicting announcement
+ val conflictingAnnouncement = buildConflictingAnnouncementForCustomHost()
+ packetReader.sendResponse(conflictingAnnouncement)
+
+ // Expect to see probes (RFC6762 9., service is reset to probing state)
+ assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+ "Probe not received within timeout after conflict")
+
+ // Send the conflicting packet again to reply to the probe
+ packetReader.sendResponse(conflictingAnnouncement)
+
+ val newRegistration =
+ registrationRecord
+ .expectCallbackEventually<ServiceRegistered>(REGISTRATION_TIMEOUT_MS) {
+ it.serviceInfo.serviceName == serviceName
+ && it.serviceInfo.hostname.let { hostname ->
+ hostname != null
+ && hostname.startsWith(customHostname)
+ && hostname != customHostname
+ }
+ }
+
+ val resolvedInfo = resolveService(discoveredInfo)
+ assertEquals(newRegistration.serviceInfo.serviceName, resolvedInfo.serviceName)
+ assertEquals(newRegistration.serviceInfo.hostname, resolvedInfo.hostname)
+
+ discoveryRecord.assertNoCallback()
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+ discoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanupStep {
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ }
+ }
+
+ @Test
+ fun testRegisterServiceWithCustomHostNoAddresses_noConflictAfterProbing_notRenamed() {
+ val si = makeTestServiceInfo(testNetwork1.network).apply {
+ hostname = customHostname
+ }
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ val discoveryRecord = NsdDiscoveryRecord()
+ val registeredService = registerService(registrationRecord, si)
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+ handlerThread.waitForIdle(TIMEOUT_MS)
+
+ tryTest {
+ assertNotNull(packetReader.pollForAdvertisement(serviceName, serviceType),
+ "No announcements sent after initial probing")
+
+ assertEquals(si.serviceName, registeredService.serviceName)
+ assertEquals(si.hostname, registeredService.hostname)
+
+ // Send a conflicting announcement
+ val conflictingAnnouncement = buildConflictingAnnouncementForCustomHost()
+ packetReader.sendResponse(conflictingAnnouncement)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, { it.run() }, discoveryRecord)
+
+ // The service is not renamed
+ discoveryRecord.waitForServiceDiscovered(si.serviceName, serviceType)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+ discoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanupStep {
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ }
+ }
+
// Test that even if only a PTR record is received as a reply when discovering, without the
// SRV, TXT, address records as recommended (but not mandated) by RFC 6763 12, the service can
// still be discovered.
@@ -1286,7 +1604,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)
@@ -1349,6 +1668,681 @@
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>()
+ }
+ }
+
+ @Test
+ fun testReplyWhenKnownAnswerSuppressionFlagSet() {
+ // The flag may be removed in the future but known-answer suppression should be enabled by
+ // default in that case. The rule will reset flags automatically on teardown.
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_known_answer_suppression", "1")
+ 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 with a known answer. Expect to receive a response containing TXT record
+ only.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
+ scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR',
+ qclass=0x8001) /
+ scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
+ qclass=0x8001),
+ an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
+ rdata='NsdTest123456789._nmt123456789._tcp.local')
+ )).hex()
+ */
+ val query = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d74313233343" +
+ "536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
+ "d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d743132333" +
+ "43536373839045f746370056c6f63616c00000c000100001194002b104e73645465737431323" +
+ "33435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(query)
+
+ val testSrcAddr = makeLinkLocalAddressOfOtherDeviceOnPrefix(testNetwork1.network)
+ nsResponder = NSResponder(packetReader, mapOf(
+ testSrcAddr to MacAddress.fromString("01:02:03:04:05:06")
+ )).apply { start() }
+
+ packetReader.sendResponse(buildMdnsPacket(query, 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("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT) &&
+ !pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.dstAddr == testSrcAddr
+ }
+ assertNotNull(reply)
+
+ /*
+ Send a query with a known answer (TTL is less than half). Expect to receive a response
+ containing both PTR and TXT records.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd =
+ scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR',
+ qclass=0x8001) /
+ scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
+ qclass=0x8001),
+ an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=2150,
+ rdata='NsdTest123456789._nmt123456789._tcp.local')
+ )).hex()
+ */
+ val query2 = HexDump.hexStringToByteArray("0000000000020001000000000d5f6e6d7431323334" +
+ "3536373839045f746370056c6f63616c00000c8001104e736454657374313233343536373839" +
+ "0d5f6e6d74313233343536373839045f746370056c6f63616c00001080010d5f6e6d74313233" +
+ "343536373839045f746370056c6f63616c00000c000100000866002b104e7364546573743132" +
+ "333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(query2)
+
+ packetReader.sendResponse(buildMdnsPacket(query2, 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 reply2 = packetReader.pollForMdnsPacket { pkt ->
+ pkt.isReplyFor("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT) &&
+ pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.dstAddr == testSrcAddr
+ }
+ assertNotNull(reply2)
+ } cleanup {
+ nsResponder?.stop()
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ }
+ }
+
+ @Test
+ fun testReplyWithMultipacketWhenKnownAnswerSuppressionFlagSet() {
+ // The flag may be removed in the future but known-answer suppression should be enabled by
+ // default in that case. The rule will reset flags automatically on teardown.
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_known_answer_suppression", "1")
+ 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 with truncated bit set.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, tc=1, qd=
+ scapy.DNSQR(qname='_nmt123456789._tcp.local', qtype='PTR',
+ qclass=0x8001) /
+ scapy.DNSQR(qname='NsdTest123456789._nmt123456789._tcp.local', qtype='TXT',
+ qclass=0x8001)
+ )).hex()
+ */
+ val query = HexDump.hexStringToByteArray("0000020000020000000000000d5f6e6d74313233343" +
+ "536373839045f746370056c6f63616c00000c8001104e7364546573743132333435363738390" +
+ "d5f6e6d74313233343536373839045f746370056c6f63616c0000108001")
+ replaceServiceNameAndTypeWithTestSuffix(query)
+ /*
+ Send a known answer packet (other service) with truncated bit set.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, tc=1, qd=None,
+ an = scapy.DNSRR(rrname='_test._tcp.local', type='PTR', ttl=4500,
+ rdata='NsdTest._test._tcp.local')
+ )).hex()
+ */
+ val knownAnswer1 = HexDump.hexStringToByteArray("000002000000000100000000055f74657374" +
+ "045f746370056c6f63616c00000c000100001194001a074e736454657374055f74657374045f" +
+ "746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(knownAnswer1)
+ /*
+ Send a known answer packet.
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=0, aa=0, qd=None,
+ an = scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=4500,
+ rdata='NsdTest123456789._nmt123456789._tcp.local')
+ )).hex()
+ */
+ val knownAnswer2 = HexDump.hexStringToByteArray("0000000000000001000000000d5f6e6d7431" +
+ "3233343536373839045f746370056c6f63616c00000c000100001194002b104e736454657374" +
+ "3132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+ replaceServiceNameAndTypeWithTestSuffix(knownAnswer2)
+
+ val testSrcAddr = makeLinkLocalAddressOfOtherDeviceOnPrefix(testNetwork1.network)
+ nsResponder = NSResponder(packetReader, mapOf(
+ testSrcAddr to MacAddress.fromString("01:02:03:04:05:06")
+ )).apply { start() }
+
+ packetReader.sendResponse(buildMdnsPacket(query, testSrcAddr))
+ packetReader.sendResponse(buildMdnsPacket(knownAnswer1, testSrcAddr))
+ packetReader.sendResponse(buildMdnsPacket(knownAnswer2, 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("$serviceName.$serviceType.local", DnsResolver.TYPE_TXT) &&
+ !pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.dstAddr == testSrcAddr
+ }
+ assertNotNull(reply)
+ } cleanup {
+ nsResponder?.stop()
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ }
+ }
+
+ @Test
+ fun testQueryWhenKnownAnswerSuppressionFlagSet() {
+ // The flag may be removed in the future but known-answer suppression should be enabled by
+ // default in that case. The rule will reset flags automatically on teardown.
+ deviceConfigRule.setConfig(NAMESPACE_TETHERING, "test_nsd_query_with_known_answer", "1")
+
+ // Register service on testNetwork1
+ val discoveryRecord = NsdDiscoveryRecord()
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+ handlerThread.waitForIdle(TIMEOUT_MS)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, { it.run() }, discoveryRecord)
+
+ tryTest {
+ discoveryRecord.expectCallback<DiscoveryStarted>()
+ assertNotNull(packetReader.pollForQuery("$serviceType.local", DnsResolver.TYPE_PTR))
+ /*
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
+ scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=120,
+ rdata='NsdTest123456789._nmt123456789._tcp.local'))).hex()
+ */
+ val ptrResponsePayload = HexDump.hexStringToByteArray("0000840000000001000000000d5f6e" +
+ "6d74313233343536373839045f746370056c6f63616c00000c000100000078002b104e736454" +
+ "6573743132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+
+ replaceServiceNameAndTypeWithTestSuffix(ptrResponsePayload)
+ packetReader.sendResponse(buildMdnsPacket(ptrResponsePayload))
+
+ val serviceFound = discoveryRecord.expectCallback<ServiceFound>()
+ serviceFound.serviceInfo.let {
+ assertEquals(serviceName, it.serviceName)
+ // Discovered service types have a dot at the end
+ assertEquals("$serviceType.", it.serviceType)
+ assertEquals(testNetwork1.network, it.network)
+ // ServiceFound does not provide port, address or attributes (only information
+ // available in the PTR record is included in that callback, regardless of whether
+ // other records exist).
+ assertEquals(0, it.port)
+ assertEmpty(it.hostAddresses)
+ assertEquals(0, it.attributes.size)
+ }
+
+ // Expect the second query with a known answer
+ val query = packetReader.pollForMdnsPacket { pkt ->
+ pkt.isQueryFor("$serviceType.local", DnsResolver.TYPE_PTR) &&
+ pkt.isReplyFor("$serviceType.local", DnsResolver.TYPE_PTR)
+ }
+ assertNotNull(query)
+ } cleanup {
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+ discoveryRecord.expectCallback<DiscoveryStopped>()
+ }
+ }
+
+ 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
+ }
+
+ @Test
+ fun testAdvertisingAndDiscovery_servicesWithCustomHost_customHostAddressesFound() {
+ val hostAddresses1 = listOf(
+ parseNumericAddress("192.0.2.23"),
+ parseNumericAddress("2001:db8::1"),
+ parseNumericAddress("2001:db8::2"))
+ val hostAddresses2 = listOf(
+ parseNumericAddress("192.0.2.24"),
+ parseNumericAddress("2001:db8::3"))
+ val si1 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceName = serviceName
+ it.serviceType = serviceType
+ it.port = TEST_PORT
+ it.hostname = customHostname
+ it.hostAddresses = hostAddresses1
+ }
+ val si2 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceName = serviceName2
+ it.serviceType = serviceType
+ it.port = TEST_PORT + 1
+ it.hostname = customHostname2
+ it.hostAddresses = hostAddresses2
+ }
+ val registrationRecord1 = NsdRegistrationRecord()
+ val registrationRecord2 = NsdRegistrationRecord()
+
+ val discoveryRecord1 = NsdDiscoveryRecord()
+ val discoveryRecord2 = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord1, si1)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord1)
+
+ val discoveredInfo = discoveryRecord1.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ val resolvedInfo = resolveService(discoveredInfo)
+
+ assertEquals(TEST_PORT, resolvedInfo.port)
+ assertEquals(si1.hostname, resolvedInfo.hostname)
+ assertAddressEquals(hostAddresses1, resolvedInfo.hostAddresses)
+
+ registerService(registrationRecord2, si2)
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord2)
+
+ val discoveredInfo2 = discoveryRecord2.waitForServiceDiscovered(
+ serviceName2, serviceType, testNetwork1.network)
+ val resolvedInfo2 = resolveService(discoveredInfo2)
+
+ assertEquals(TEST_PORT + 1, resolvedInfo2.port)
+ assertEquals(si2.hostname, resolvedInfo2.hostname)
+ assertAddressEquals(hostAddresses2, resolvedInfo2.hostAddresses)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord1)
+ nsdManager.stopServiceDiscovery(discoveryRecord2)
+
+ discoveryRecord1.expectCallbackEventually<DiscoveryStopped>()
+ discoveryRecord2.expectCallbackEventually<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord1)
+ nsdManager.unregisterService(registrationRecord2)
+ }
+ }
+
+ @Test
+ fun testAdvertisingAndDiscovery_multipleRegistrationsForSameCustomHost_unionOfAddressesFound() {
+ val hostAddresses1 = listOf(
+ parseNumericAddress("192.0.2.23"),
+ parseNumericAddress("2001:db8::1"),
+ parseNumericAddress("2001:db8::2"))
+ val hostAddresses2 = listOf(
+ parseNumericAddress("192.0.2.24"),
+ parseNumericAddress("2001:db8::3"))
+ val hostAddresses3 = listOf(
+ parseNumericAddress("2001:db8::3"),
+ parseNumericAddress("2001:db8::5"))
+ val si1 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.hostname = customHostname
+ it.hostAddresses = hostAddresses1
+ }
+ val si2 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceName = serviceName
+ it.serviceType = serviceType
+ it.port = TEST_PORT
+ it.hostname = customHostname
+ it.hostAddresses = hostAddresses2
+ }
+ val si3 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceName = serviceName3
+ it.serviceType = serviceType
+ it.port = TEST_PORT + 1
+ it.hostname = customHostname
+ it.hostAddresses = hostAddresses3
+ }
+
+ val registrationRecord1 = NsdRegistrationRecord()
+ val registrationRecord2 = NsdRegistrationRecord()
+ val registrationRecord3 = NsdRegistrationRecord()
+
+ val discoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord1, si1)
+ registerService(registrationRecord2, si2)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord)
+
+ val discoveredInfo1 = discoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ val resolvedInfo1 = resolveService(discoveredInfo1)
+
+ assertEquals(TEST_PORT, resolvedInfo1.port)
+ assertEquals(si1.hostname, resolvedInfo1.hostname)
+ assertAddressEquals(
+ hostAddresses1 + hostAddresses2,
+ resolvedInfo1.hostAddresses)
+
+ registerService(registrationRecord3, si3)
+
+ val discoveredInfo2 = discoveryRecord.waitForServiceDiscovered(
+ serviceName3, serviceType, testNetwork1.network)
+ val resolvedInfo2 = resolveService(discoveredInfo2)
+
+ assertEquals(TEST_PORT + 1, resolvedInfo2.port)
+ assertEquals(si2.hostname, resolvedInfo2.hostname)
+ assertAddressEquals(
+ hostAddresses1 + hostAddresses2 + hostAddresses3,
+ resolvedInfo2.hostAddresses)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+
+ discoveryRecord.expectCallbackEventually<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord1)
+ nsdManager.unregisterService(registrationRecord2)
+ nsdManager.unregisterService(registrationRecord3)
+ }
+ }
+
+ @Test
+ fun testAdvertisingAndDiscovery_servicesWithTheSameCustomHostAddressOmitted_addressesFound() {
+ val hostAddresses = listOf(
+ parseNumericAddress("192.0.2.23"),
+ parseNumericAddress("2001:db8::1"),
+ parseNumericAddress("2001:db8::2"))
+ val si1 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceType = serviceType
+ it.serviceName = serviceName
+ it.port = TEST_PORT
+ it.hostname = customHostname
+ it.hostAddresses = hostAddresses
+ }
+ val si2 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceType = serviceType
+ it.serviceName = serviceName2
+ it.port = TEST_PORT + 1
+ it.hostname = customHostname
+ }
+
+ val registrationRecord1 = NsdRegistrationRecord()
+ val registrationRecord2 = NsdRegistrationRecord()
+
+ val discoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord1, si1)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord)
+
+ val discoveredInfo1 = discoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ val resolvedInfo1 = resolveService(discoveredInfo1)
+
+ assertEquals(serviceName, discoveredInfo1.serviceName)
+ assertEquals(TEST_PORT, resolvedInfo1.port)
+ assertEquals(si1.hostname, resolvedInfo1.hostname)
+ assertAddressEquals(hostAddresses, resolvedInfo1.hostAddresses)
+
+ registerService(registrationRecord2, si2)
+
+ val discoveredInfo2 = discoveryRecord.waitForServiceDiscovered(
+ serviceName2, serviceType, testNetwork1.network)
+ val resolvedInfo2 = resolveService(discoveredInfo2)
+
+ assertEquals(serviceName2, discoveredInfo2.serviceName)
+ assertEquals(TEST_PORT + 1, resolvedInfo2.port)
+ assertEquals(si2.hostname, resolvedInfo2.hostname)
+ assertAddressEquals(hostAddresses, resolvedInfo2.hostAddresses)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+
+ discoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord1)
+ nsdManager.unregisterService(registrationRecord2)
+ }
+ }
+
+ @Test
+ fun testRegisterService_registerImmediatelyAfterUnregister_serviceFound() {
+ val info1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceName = "service11111"
+ port = 11111
+ }
+ val info2 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceName = "service22222"
+ port = 22222
+ }
+ val registrationRecord1 = NsdRegistrationRecord()
+ val discoveryRecord1 = NsdDiscoveryRecord()
+ val registrationRecord2 = NsdRegistrationRecord()
+ val discoveryRecord2 = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord1, info1)
+ nsdManager.discoverServices(serviceType,
+ NsdManager.PROTOCOL_DNS_SD, testNetwork1.network, { it.run() },
+ discoveryRecord1)
+ discoveryRecord1.waitForServiceDiscovered(info1.serviceName,
+ serviceType, testNetwork1.network)
+ nsdManager.stopServiceDiscovery(discoveryRecord1)
+
+ nsdManager.unregisterService(registrationRecord1)
+ registerService(registrationRecord2, info2)
+ nsdManager.discoverServices(serviceType,
+ NsdManager.PROTOCOL_DNS_SD, testNetwork1.network, { it.run() },
+ discoveryRecord2)
+ val infoDiscovered = discoveryRecord2.waitForServiceDiscovered(info2.serviceName,
+ serviceType, testNetwork1.network)
+ val infoResolved = resolveService(infoDiscovered)
+ assertEquals(22222, infoResolved.port)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord2)
+ discoveryRecord2.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord2)
+ }
+ }
+
+ @Test
+ fun testAdvertisingAndDiscovery_reregisterCustomHostWithDifferentAddresses_newAddressesFound() {
+ val si1 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.hostname = customHostname
+ it.hostAddresses = listOf(
+ parseNumericAddress("192.0.2.23"),
+ parseNumericAddress("2001:db8::1"))
+ }
+ val si2 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.serviceName = serviceName
+ it.serviceType = serviceType
+ it.hostname = customHostname
+ it.port = TEST_PORT
+ }
+ val si3 = NsdServiceInfo().also {
+ it.network = testNetwork1.network
+ it.hostname = customHostname
+ it.hostAddresses = listOf(
+ parseNumericAddress("192.0.2.24"),
+ parseNumericAddress("2001:db8::2"))
+ }
+
+ val registrationRecord1 = NsdRegistrationRecord()
+ val registrationRecord2 = NsdRegistrationRecord()
+ val registrationRecord3 = NsdRegistrationRecord()
+
+ val discoveryRecord = NsdDiscoveryRecord()
+
+ tryTest {
+ registerService(registrationRecord1, si1)
+ registerService(registrationRecord2, si2)
+
+ nsdManager.unregisterService(registrationRecord1)
+ registrationRecord1.expectCallback<ServiceUnregistered>()
+
+ registerService(registrationRecord3, si3)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord)
+ val discoveredInfo = discoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ val resolvedInfo = resolveService(discoveredInfo)
+
+ assertEquals(serviceName, discoveredInfo.serviceName)
+ assertEquals(TEST_PORT, resolvedInfo.port)
+ assertEquals(customHostname, resolvedInfo.hostname)
+ assertAddressEquals(
+ listOf(parseNumericAddress("192.0.2.24"), parseNumericAddress("2001:db8::2")),
+ resolvedInfo.hostAddresses)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+ discoveryRecord.expectCallbackEventually<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord2)
+ nsdManager.unregisterService(registrationRecord3)
+ }
+ }
+
+ @Test
+ fun testServiceTypeClientRemovedAfterSocketDestroyed() {
+ val si = makeTestServiceInfo(testNetwork1.network)
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ registerService(registrationRecord, si)
+ // Register multiple discovery requests.
+ val discoveryRecord1 = NsdDiscoveryRecord()
+ val discoveryRecord2 = NsdDiscoveryRecord()
+ val discoveryRecord3 = NsdDiscoveryRecord()
+ nsdManager.discoverServices("_test1._tcp", NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, { it.run() }, discoveryRecord1)
+ nsdManager.discoverServices("_test2._tcp", NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, { it.run() }, discoveryRecord2)
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord3)
+
+ tryTest {
+ discoveryRecord1.expectCallback<DiscoveryStarted>()
+ discoveryRecord2.expectCallback<DiscoveryStarted>()
+ discoveryRecord3.expectCallback<DiscoveryStarted>()
+ val foundInfo = discoveryRecord3.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ assertEquals(testNetwork1.network, foundInfo.network)
+ // Verify that associated ServiceTypeClients has been created for testNetwork1.
+ assertTrue("No serviceTypeClients for testNetwork1.",
+ hasServiceTypeClientsForNetwork(
+ getServiceTypeClients(), testNetwork1.network))
+
+ // Disconnect testNetwork1
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ testNetwork1.close(cm)
+ }
+
+ // Verify that no ServiceTypeClients for testNetwork1.
+ discoveryRecord3.expectCallback<ServiceLost>()
+ assertFalse("Still has serviceTypeClients for testNetwork1.",
+ hasServiceTypeClientsForNetwork(
+ getServiceTypeClients(), testNetwork1.network))
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord1)
+ nsdManager.stopServiceDiscovery(discoveryRecord2)
+ nsdManager.stopServiceDiscovery(discoveryRecord3)
+ discoveryRecord1.expectCallback<DiscoveryStopped>()
+ discoveryRecord2.expectCallback<DiscoveryStopped>()
+ discoveryRecord3.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ }
+ }
+
+ private fun hasServiceTypeClientsForNetwork(clients: List<String>, network: Network): Boolean {
+ return clients.any { client -> client.substring(
+ client.indexOf("network=") + "network=".length,
+ client.indexOf("interfaceIndex=") - 1) == network.getNetId().toString()
+ }
+ }
+
+ /**
+ * Get ServiceTypeClient logs from the system dump servicediscovery section.
+ *
+ * The sample output:
+ * ServiceTypeClient: Type{_nmt079019787._tcp.local} \
+ * SocketKey{ network=116 interfaceIndex=68 } with 1 listeners.
+ * ServiceTypeClient: Type{_nmt079019787._tcp.local} \
+ * SocketKey{ network=115 interfaceIndex=67 } with 1 listeners.
+ */
+ private fun getServiceTypeClients(): List<String> {
+ return SystemUtil.runShellCommand(
+ InstrumentationRegistry.getInstrumentation(), "dumpsys servicediscovery")
+ .split("\n").mapNotNull { line ->
+ line.indexOf("ServiceTypeClient:").let { idx ->
+ if (idx == -1) null
+ else line.substring(idx)
+ }
+ }
+ }
+
private fun buildConflictingAnnouncement(): ByteBuffer {
/*
Generated with:
@@ -1365,6 +2359,22 @@
return buildMdnsPacket(mdnsPayload)
}
+ private fun buildConflictingAnnouncementForCustomHost(): ByteBuffer {
+ /*
+ Generated with scapy:
+ raw(DNS(rd=0, qr=1, aa=1, qd = None, an =
+ DNSRR(rrname='NsdTestHost123456789.local', type=28, rclass=1, ttl=120,
+ rdata='2001:db8::321')
+ )).hex()
+ */
+ val mdnsPayload = HexDump.hexStringToByteArray("000084000000000100000000144e7364" +
+ "54657374486f7374313233343536373839056c6f63616c00001c000100000078001020010db80000" +
+ "00000000000000000321")
+ replaceCustomHostnameWithTestSuffix(mdnsPayload)
+
+ return buildMdnsPacket(mdnsPayload)
+ }
+
/**
* Replaces occurrences of "NsdTest123456789" and "_nmt123456789" in mDNS payload with the
* actual random name and type that are used by the test.
@@ -1381,6 +2391,19 @@
replaceAll(packetBuffer, testPacketTypePrefix, encodedTypePrefix)
}
+ /**
+ * Replaces occurrences of "NsdTestHost123456789" in mDNS payload with the
+ * actual random host name that are used by the test.
+ */
+ private fun replaceCustomHostnameWithTestSuffix(mdnsPayload: ByteArray) {
+ // Test custom hostnames have consistent length and are always ASCII
+ val testPacketName = "NsdTestHost123456789".encodeToByteArray()
+ val encodedHostname = customHostname.encodeToByteArray()
+
+ val packetBuffer = ByteBuffer.wrap(mdnsPayload)
+ replaceAll(packetBuffer, testPacketName, encodedHostname)
+ }
+
private tailrec fun replaceAll(buffer: ByteBuffer, source: ByteArray, replacement: ByteArray) {
assertEquals(source.size, replacement.size)
val index = buffer.array().indexOf(source)
@@ -1393,7 +2416,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)
@@ -1408,7 +2434,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)
@@ -1476,3 +2502,9 @@
if (this == null) return ""
return String(this, StandardCharsets.UTF_8)
}
+
+private fun assertAddressEquals(expected: List<InetAddress>, actual: List<InetAddress>) {
+ // No duplicate addresses in the actual address list
+ assertEquals(actual.toSet().size, actual.size)
+ assertEquals(expected.toSet(), actual.toSet())
+}
diff --git a/tests/cts/net/src/android/net/cts/OffloadServiceInfoTest.kt b/tests/cts/net/src/android/net/cts/OffloadServiceInfoTest.kt
new file mode 100644
index 0000000..36de4f2
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/OffloadServiceInfoTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts
+
+import android.net.nsd.OffloadEngine.OFFLOAD_TYPE_FILTER_QUERIES
+import android.net.nsd.OffloadServiceInfo
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** CTS tests for {@link OffloadServiceInfo}. */
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@ConnectivityModuleTest
+class OffloadServiceInfoTest {
+ @Test
+ fun testCreateOffloadServiceInfo() {
+ val offloadServiceInfo = OffloadServiceInfo(
+ OffloadServiceInfo.Key("_testService", "_testType"),
+ listOf("_sub1", "_sub2"),
+ "Android.local",
+ byteArrayOf(0x1, 0x2, 0x3),
+ 1 /* priority */,
+ OFFLOAD_TYPE_FILTER_QUERIES.toLong()
+ )
+
+ assertEquals(OffloadServiceInfo.Key("_testService", "_testType"), offloadServiceInfo.key)
+ assertEquals(listOf("_sub1", "_sub2"), offloadServiceInfo.subtypes)
+ assertEquals("Android.local", offloadServiceInfo.hostname)
+ assertContentEquals(byteArrayOf(0x1, 0x2, 0x3), offloadServiceInfo.offloadPayload)
+ assertEquals(1, offloadServiceInfo.priority)
+ assertEquals(OFFLOAD_TYPE_FILTER_QUERIES.toLong(), offloadServiceInfo.offloadType)
+ }
+}
diff --git a/tests/cts/net/util/Android.bp b/tests/cts/net/util/Android.bp
index fffd30f..644634b 100644
--- a/tests/cts/net/util/Android.bp
+++ b/tests/cts/net/util/Android.bp
@@ -16,12 +16,16 @@
// Common utilities for cts net tests.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
java_library {
name: "cts-net-utils",
- srcs: ["java/**/*.java", "java/**/*.kt"],
+ srcs: [
+ "java/**/*.java",
+ "java/**/*.kt",
+ ],
static_libs: [
"compatibility-device-util-axt",
"junit",
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
index 96330e2..670889f 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -173,21 +173,39 @@
return cb;
}
- // Toggle WiFi twice, leaving it in the state it started in
- public void toggleWifi() throws Exception {
- if (mWifiManager.isWifiEnabled()) {
- Network wifiNetwork = getWifiNetwork();
- // Ensure system default network is WIFI because it's expected in disconnectFromWifi()
- expectNetworkIsSystemDefault(wifiNetwork);
- disconnectFromWifi(wifiNetwork);
- connectToWifi();
- } else {
- connectToWifi();
- Network wifiNetwork = getWifiNetwork();
- // Ensure system default network is WIFI because it's expected in disconnectFromWifi()
- expectNetworkIsSystemDefault(wifiNetwork);
- disconnectFromWifi(wifiNetwork);
+ /**
+ * Toggle Wi-Fi off and on, waiting for the {@link ConnectivityManager#CONNECTIVITY_ACTION}
+ * broadcast in both cases.
+ */
+ public void reconnectWifiAndWaitForConnectivityAction() throws Exception {
+ assertTrue(mWifiManager.isWifiEnabled());
+ Network wifiNetwork = getWifiNetwork();
+ // Ensure system default network is WIFI because it's expected in disconnectFromWifi()
+ expectNetworkIsSystemDefault(wifiNetwork);
+ disconnectFromWifi(wifiNetwork, true /* expectLegacyBroadcast */);
+ connectToWifi(true /* expectLegacyBroadcast */);
+ }
+
+ /**
+ * Turn Wi-Fi off, then back on and make sure it connects, if it is supported.
+ */
+ public void reconnectWifiIfSupported() throws Exception {
+ if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+ return;
}
+ disableWifi();
+ ensureWifiConnected();
+ }
+
+ /**
+ * Turn cell data off, then back on and make sure it connects, if it is supported.
+ */
+ public void reconnectCellIfSupported() throws Exception {
+ if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+ return;
+ }
+ setMobileDataEnabled(false);
+ setMobileDataEnabled(true);
}
public Network expectNetworkIsSystemDefault(Network network)
@@ -390,40 +408,10 @@
return network;
}
- public Network connectToCell() throws InterruptedException {
- if (cellConnectAttempted()) {
- mCm.unregisterNetworkCallback(mCellNetworkCallback);
- }
- NetworkRequest cellRequest = new NetworkRequest.Builder()
- .addTransportType(TRANSPORT_CELLULAR)
- .addCapability(NET_CAPABILITY_INTERNET)
- .build();
- mCellNetworkCallback = new TestNetworkCallback();
- mCm.requestNetwork(cellRequest, mCellNetworkCallback);
- final Network cellNetwork = mCellNetworkCallback.waitForAvailable();
- assertNotNull("Cell network not available. " +
- "Please ensure the device has working mobile data.", cellNetwork);
- return cellNetwork;
- }
-
- public void disconnectFromCell() {
- if (!cellConnectAttempted()) {
- throw new IllegalStateException("Cell connection not attempted");
- }
- mCm.unregisterNetworkCallback(mCellNetworkCallback);
- mCellNetworkCallback = null;
- }
-
public boolean cellConnectAttempted() {
return mCellNetworkCallback != null;
}
- public void tearDown() {
- if (cellConnectAttempted()) {
- disconnectFromCell();
- }
- }
-
private NetworkRequest makeWifiNetworkRequest() {
return new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
diff --git a/tests/cts/netpermission/internetpermission/Android.bp b/tests/cts/netpermission/internetpermission/Android.bp
index 5314396..7d5ca2f 100644
--- a/tests/cts/netpermission/internetpermission/Android.bp
+++ b/tests/cts/netpermission/internetpermission/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/netpermission/updatestatspermission/Android.bp b/tests/cts/netpermission/updatestatspermission/Android.bp
index 40474db..2fde1ce 100644
--- a/tests/cts/netpermission/updatestatspermission/Android.bp
+++ b/tests/cts/netpermission/updatestatspermission/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index 4284f56..1023173 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -46,6 +47,7 @@
// Change to system current when TetheringManager move to bootclass path.
platform_apis: true,
+ min_sdk_version: "30",
host_required: ["net-tests-utils-host-common"],
}
@@ -79,8 +81,8 @@
// Tethering CTS tests for development and release. These tests always target the platform SDK
// version, and are subject to all the restrictions appropriate to that version. Before SDK
-// finalization, these tests have a min_sdk_version of 10000, and cannot be installed on release
-// devices.
+// finalization, these tests have a min_sdk_version of 10000, but they can still be installed on
+// release devices as their min_sdk_version is set to a production version.
android_test {
name: "CtsTetheringTest",
defaults: ["CtsTetheringTestDefaults"],
@@ -92,6 +94,14 @@
// Tag this module as a cts test artifact
test_suites: [
"cts",
+ "mts-dnsresolver",
+ "mts-networking",
+ "mts-tethering",
+ "mts-wifi",
+ "mcts-dnsresolver",
+ "mcts-networking",
+ "mcts-tethering",
+ "mcts-wifi",
"general-tests",
],
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index 274596f..81608f7 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -71,6 +71,8 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.testutils.ParcelUtils;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -236,6 +238,26 @@
}
@Test
+ public void testTetheringRequestParcelable() {
+ final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
+ final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
+ final TetheringRequest unparceled = new TetheringRequest.Builder(TETHERING_USB)
+ .setStaticIpv4Addresses(localAddr, clientAddr)
+ .setExemptFromEntitlementCheck(true)
+ .setShouldShowEntitlementUi(false).build();
+ final TetheringRequest parceled = ParcelUtils.parcelingRoundTrip(unparceled);
+ assertEquals(unparceled.getTetheringType(), parceled.getTetheringType());
+ assertEquals(unparceled.getConnectivityScope(), parceled.getConnectivityScope());
+ assertEquals(unparceled.getLocalIpv4Address(), parceled.getLocalIpv4Address());
+ assertEquals(unparceled.getClientStaticIpv4Address(),
+ parceled.getClientStaticIpv4Address());
+ assertEquals(unparceled.isExemptFromEntitlementCheck(),
+ parceled.isExemptFromEntitlementCheck());
+ assertEquals(unparceled.getShouldShowEntitlementUi(),
+ parceled.getShouldShowEntitlementUi());
+ }
+
+ @Test
public void testRegisterTetheringEventCallback() throws Exception {
final TestTetheringEventCallback tetherEventCallback =
mCtsTetheringUtils.registerTetheringEventCallback();
diff --git a/tests/deflake/Android.bp b/tests/deflake/Android.bp
index 8205f1cf..726e504 100644
--- a/tests/deflake/Android.bp
+++ b/tests/deflake/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index f705e34..349529dd 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -74,7 +75,10 @@
java_library {
name: "frameworks-net-integration-testutils",
defaults: ["framework-connectivity-test-defaults"],
- srcs: ["util/**/*.java", "util/**/*.kt"],
+ srcs: [
+ "util/**/*.java",
+ "util/**/*.kt",
+ ],
static_libs: [
"androidx.annotation_annotation",
"androidx.test.rules",
diff --git a/tests/integration/AndroidManifest.xml b/tests/integration/AndroidManifest.xml
index cea83c7..1821329 100644
--- a/tests/integration/AndroidManifest.xml
+++ b/tests/integration/AndroidManifest.xml
@@ -42,6 +42,9 @@
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<!-- Register UidFrozenStateChangedCallback -->
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
+ <!-- Permission required for CTS test - NetworkStatsIntegrationTest -->
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/>
<application android:debuggable="true">
<uses-library android:name="android.test.runner"/>
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..d2e46af 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,37 @@
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.server.connectivity.SatelliteAccessController
+import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.DeviceInfoUtils
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.tryTest
+import java.util.function.BiConsumer
+import java.util.function.Consumer
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
@@ -74,12 +83,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 +97,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 +125,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 +207,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 +218,63 @@
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 isChangeEnabled(changeId: Long, uid: Int) = true
+
+ 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,
+ requestRestrictedWifiEnabled: Boolean,
+ listener: BiConsumer<Int, Int>,
+ handler: Handler
+ ): CarrierPrivilegeAuthenticator {
+ return CarrierPrivilegeAuthenticator(context,
+ object : CarrierPrivilegeAuthenticator.Dependencies() {
+ override fun makeHandlerThread(): HandlerThread =
+ super.makeHandlerThread().also { handlerThreads.add(it) }
+ },
+ tm, TelephonyManagerShimImpl.newInstance(tm),
+ requestRestrictedWifiEnabled, listener, handler)
+ }
+
+ override fun makeSatelliteAccessController(
+ context: Context,
+ updateSatellitePreferredUid: Consumer<MutableSet<Int>>?,
+ connectivityServiceInternalHandler: Handler
+ ): SatelliteAccessController? = mock(
+ SatelliteAccessController::class.java)
}
@After
fun tearDown() {
nsInstrumentation.clearAllState()
ConnectivityResources.setResourcesContextForTest(null)
+ handlerThreads.forEach {
+ it.quitSafely()
+ it.join()
+ }
+ handlerThreads.clear()
}
@Test
@@ -254,13 +296,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 +339,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/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt b/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt
index 104d063..3d948ba 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt
@@ -18,10 +18,14 @@
import android.app.Service
import android.content.Intent
+import androidx.annotation.GuardedBy
+import com.android.testutils.quitExecutorServices
+import com.android.testutils.quitThreads
import java.net.URL
import java.util.Collections
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
+import java.util.concurrent.ExecutorService
import kotlin.collections.ArrayList
import kotlin.test.fail
@@ -37,7 +41,12 @@
.run {
withDefault { key -> getOrPut(key) { ConcurrentLinkedQueue() } }
}
- private val httpRequestUrls = Collections.synchronizedList(ArrayList<String>())
+ private val httpRequestUrls = Collections.synchronizedList(mutableListOf<String>())
+
+ @GuardedBy("networkMonitorThreads")
+ private val networkMonitorThreads = mutableListOf<Thread>()
+ @GuardedBy("networkMonitorExecutorServices")
+ private val networkMonitorExecutorServices = mutableListOf<ExecutorService>()
/**
* Called when an HTTP request is being processed by NetworkMonitor. Returns the response
@@ -52,10 +61,47 @@
}
/**
+ * Called when NetworkMonitor creates a new Thread.
+ */
+ fun onNetworkMonitorThreadCreated(thread: Thread) {
+ synchronized(networkMonitorThreads) {
+ networkMonitorThreads.add(thread)
+ }
+ }
+
+ /**
+ * Called when NetworkMonitor creates a new ExecutorService.
+ */
+ fun onNetworkMonitorExecutorServiceCreated(executorService: ExecutorService) {
+ synchronized(networkMonitorExecutorServices) {
+ networkMonitorExecutorServices.add(executorService)
+ }
+ }
+
+ /**
* Clear all state of this connector. This is intended for use between two tests, so all
* state should be reset as if the connector was just created.
*/
override fun clearAllState() {
+ quitThreads(
+ maxRetryCount = 3,
+ interrupt = true) {
+ synchronized(networkMonitorThreads) {
+ networkMonitorThreads.toList().also { networkMonitorThreads.clear() }
+ }
+ }
+ quitExecutorServices(
+ maxRetryCount = 3,
+ // NetworkMonitor is expected to have interrupted its executors when probing
+ // finishes, otherwise it's a thread pool leak that should be caught, so they should
+ // not need to be interrupted (the test only needs to wait for them to finish).
+ interrupt = false) {
+ synchronized(networkMonitorExecutorServices) {
+ networkMonitorExecutorServices.toList().also {
+ networkMonitorExecutorServices.clear()
+ }
+ }
+ }
httpResponses.clear()
httpRequestUrls.clear()
}
diff --git a/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
new file mode 100644
index 0000000..52e502d
--- /dev/null
+++ b/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
@@ -0,0 +1,617 @@
+/*
+ * 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.net.integrationtests
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.annotation.TargetApi
+import android.app.usage.NetworkStats
+import android.app.usage.NetworkStats.Bucket
+import android.app.usage.NetworkStats.Bucket.TAG_NONE
+import android.app.usage.NetworkStatsManager
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.TYPE_TEST
+import android.net.InetAddresses
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.NetworkTemplate
+import android.net.NetworkTemplate.MATCH_TEST
+import android.net.TestNetworkSpecifier
+import android.net.TrafficStats
+import android.os.Build
+import android.os.Process
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.net.integrationtests.NetworkStatsIntegrationTest.Direction.DOWNLOAD
+import com.android.server.net.integrationtests.NetworkStatsIntegrationTest.Direction.UPLOAD
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.PacketBridge
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.TestDnsServer
+import com.android.testutils.TestHttpServer
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
+import fi.iki.elonen.NanoHTTPD
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.net.HttpURLConnection
+import java.net.HttpURLConnection.HTTP_OK
+import java.net.InetSocketAddress
+import java.net.URL
+import java.nio.charset.Charset
+import kotlin.math.ceil
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_TAG = 0xF00D
+
+@RunWith(DevSdkIgnoreRunner::class)
+@TargetApi(Build.VERSION_CODES.S)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class NetworkStatsIntegrationTest {
+ private val TAG = NetworkStatsIntegrationTest::class.java.simpleName
+ private val LOCAL_V6ADDR =
+ LinkAddress(InetAddresses.parseNumericAddress("2001:db8::1234"), 64)
+
+ // Remote address, both the client and server will have a hallucination that
+ // they are talking to this address.
+ private val REMOTE_V6ADDR =
+ LinkAddress(InetAddresses.parseNumericAddress("dead:beef::808:808"), 64)
+ private val REMOTE_V4ADDR =
+ LinkAddress(InetAddresses.parseNumericAddress("8.8.8.8"), 32)
+ private val DEFAULT_MTU = 1500
+ private val DEFAULT_BUFFER_SIZE = 1500 // Any size greater than or equal to mtu
+ private val CONNECTION_TIMEOUT_MILLIS = 15000
+ private val TEST_DOWNLOAD_SIZE = 10000L
+ private val TEST_UPLOAD_SIZE = 20000L
+ private val HTTP_SERVER_NAME = "test.com"
+ private val HTTP_SERVER_PORT = 8080 // Use port > 1024 to avoid restrictions on system ports
+ private val DNS_INTERNAL_SERVER_PORT = 53
+ private val DNS_EXTERNAL_SERVER_PORT = 1053
+ private val TCP_ACK_SIZE = 72
+
+ // Packet overheads that are not part of the actual data transmission, these
+ // include DNS packets, TCP handshake/termination packets, and HTTP header
+ // packets. These overheads were gathered from real samples and may not
+ // be perfectly accurate because of DNS caches and TCP retransmissions, etc.
+ private val CONSTANT_PACKET_OVERHEAD = 8
+
+ // 130 is an observed average.
+ private val CONSTANT_BYTES_OVERHEAD = 130 * CONSTANT_PACKET_OVERHEAD
+ private val TOLERANCE = 1.3
+
+ // Set up the packet bridge with two IPv6 address only test networks.
+ private val inst = InstrumentationRegistry.getInstrumentation()
+ private val context = inst.getContext()
+ private val packetBridge = runAsShell(MANAGE_TEST_NETWORKS) {
+ PacketBridge(
+ context,
+ listOf(LOCAL_V6ADDR),
+ REMOTE_V6ADDR.address,
+ listOf(
+ Pair(DNS_INTERNAL_SERVER_PORT, DNS_EXTERNAL_SERVER_PORT)
+ )
+ )
+ }
+ private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+
+ // Set up DNS server for testing server and DNS64.
+ private val fakeDns = TestDnsServer(
+ packetBridge.externalNetwork,
+ InetSocketAddress(LOCAL_V6ADDR.address, DNS_EXTERNAL_SERVER_PORT)
+ ).apply {
+ start()
+ setAnswer(
+ "ipv4only.arpa",
+ listOf(IpPrefix(REMOTE_V6ADDR.address, REMOTE_V6ADDR.prefixLength).address)
+ )
+ setAnswer(HTTP_SERVER_NAME, listOf(REMOTE_V4ADDR.address))
+ }
+
+ // Start up test http server.
+ private val httpServer = TestHttpServer(
+ LOCAL_V6ADDR.address.hostAddress,
+ HTTP_SERVER_PORT
+ ).apply {
+ start()
+ }
+
+ @Before
+ fun setUp() {
+ assumeTrue(shouldRunTests())
+ packetBridge.start()
+ }
+
+ // For networkstack tests, it is not guaranteed that the tethering module will be
+ // updated at the same time. If the tethering module is not new enough, it may not contain
+ // the necessary abilities to run these tests. For example, The tests depends on test
+ // network stats being counted, which can only be achieved when they are marked as TYPE_TEST.
+ // If the tethering module does not support TYPE_TEST stats, then these tests will need
+ // to be skipped.
+ fun shouldRunTests() = cm.getNetworkInfo(packetBridge.internalNetwork)!!.type == TYPE_TEST
+
+ @After
+ fun tearDown() {
+ packetBridge.stop()
+ fakeDns.stop()
+ httpServer.stop()
+ }
+
+ private fun waitFor464XlatReady(network: Network): String {
+ val iface = cm.getLinkProperties(network)!!.interfaceName!!
+
+ // Make a network request to listen to the specific test network.
+ val nr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .setNetworkSpecifier(TestNetworkSpecifier(iface))
+ .build()
+ val testCb = TestableNetworkCallback()
+ cm.registerNetworkCallback(nr, testCb)
+
+ // Wait for the stacked address to be available.
+ testCb.eventuallyExpect<LinkPropertiesChanged> {
+ it.lp.stackedLinks.getOrNull(0)?.linkAddresses?.getOrNull(0) != null
+ }
+
+ return iface
+ }
+
+ private val Network.mtu: Int get() {
+ val lp = cm.getLinkProperties(this)!!
+ val mtuStacked = if (lp.stackedLinks[0]?.mtu != 0) lp.stackedLinks[0].mtu else DEFAULT_MTU
+ val mtuInterface = if (lp.mtu != 0) lp.mtu else DEFAULT_MTU
+ return mtuInterface.coerceAtMost(mtuStacked)
+ }
+
+ /**
+ * Verify data usage download stats with test 464xlat networks.
+ *
+ * This test starts two test networks and binds them together, the internal one is for the
+ * client to make http traffic on the test network, and the external one is for the mocked
+ * http and dns server to bind to and provide responses.
+ *
+ * After Clat setup, the client will use clat v4 address to send packets to the mocked
+ * server v4 address, which will be translated into a v6 packet by the clat daemon with
+ * NAT64 prefix learned from the mocked DNS64 response. And send to the interface.
+ *
+ * While the packets are being forwarded to the external interface, the servers will see
+ * the packets originated from the mocked v6 address, and destined to a local v6 address.
+ */
+ @Test
+ fun test464XlatTcpStats() {
+ // Wait for 464Xlat to be ready.
+ val internalInterfaceName = waitFor464XlatReady(packetBridge.internalNetwork)
+ val mtu = packetBridge.internalNetwork.mtu
+
+ val snapshotBeforeTest = StatsSnapshot(context, internalInterfaceName)
+
+ // Generate the download traffic.
+ genHttpTraffic(packetBridge.internalNetwork, uploadSize = 0L, TEST_DOWNLOAD_SIZE)
+
+ // In practice, for one way 10k download payload, the download usage is about
+ // 11222~12880 bytes, with 14~17 packets. And the upload usage is about 1279~1626 bytes
+ // with 14~17 packets, which is majorly contributed by TCP ACK packets.
+ val snapshotAfterDownload = StatsSnapshot(context, internalInterfaceName)
+ val (expectedDownloadLower, expectedDownloadUpper) = getExpectedStatsBounds(
+ TEST_DOWNLOAD_SIZE,
+ mtu,
+ DOWNLOAD
+ )
+ assertOnlyNonTaggedStatsIncreases(
+ snapshotBeforeTest,
+ snapshotAfterDownload,
+ expectedDownloadLower,
+ expectedDownloadUpper
+ )
+
+ // Generate upload traffic with tag to verify tagged data accounting as well.
+ genHttpTrafficWithTag(
+ packetBridge.internalNetwork,
+ TEST_UPLOAD_SIZE,
+ downloadSize = 0L,
+ TEST_TAG
+ )
+
+ // Verify upload data usage accounting.
+ val snapshotAfterUpload = StatsSnapshot(context, internalInterfaceName)
+ val (expectedUploadLower, expectedUploadUpper) = getExpectedStatsBounds(
+ TEST_UPLOAD_SIZE,
+ mtu,
+ UPLOAD
+ )
+ assertAllStatsIncreases(
+ snapshotAfterDownload,
+ snapshotAfterUpload,
+ expectedUploadLower,
+ expectedUploadUpper
+ )
+ }
+
+ private enum class Direction {
+ DOWNLOAD,
+ UPLOAD
+ }
+
+ private fun getExpectedStatsBounds(
+ transmittedSize: Long,
+ mtu: Int,
+ direction: Direction
+ ): Pair<BareStats, BareStats> {
+ // This is already an underestimated value since the input doesn't include TCP/IP
+ // layer overhead.
+ val txBytesLower = transmittedSize
+ // Include TCP/IP header overheads and retransmissions in the upper bound.
+ val txBytesUpper = (transmittedSize * TOLERANCE).toLong()
+ val txPacketsLower = txBytesLower / mtu + (CONSTANT_PACKET_OVERHEAD / TOLERANCE).toLong()
+ val estTransmissionPacketsUpper = ceil(txBytesUpper / mtu.toDouble()).toLong()
+ val txPacketsUpper = estTransmissionPacketsUpper +
+ (CONSTANT_PACKET_OVERHEAD * TOLERANCE).toLong()
+ // Assume ACK only sent once for the entire transmission.
+ val rxPacketsLower = 1L + (CONSTANT_PACKET_OVERHEAD / TOLERANCE).toLong()
+ // Assume ACK sent for every RX packet.
+ val rxPacketsUpper = txPacketsUpper
+ val rxBytesLower = 1L * TCP_ACK_SIZE + (CONSTANT_BYTES_OVERHEAD / TOLERANCE).toLong()
+ val rxBytesUpper = estTransmissionPacketsUpper * TCP_ACK_SIZE +
+ (CONSTANT_BYTES_OVERHEAD * TOLERANCE).toLong()
+
+ return if (direction == UPLOAD) {
+ BareStats(rxBytesLower, rxPacketsLower, txBytesLower, txPacketsLower) to
+ BareStats(rxBytesUpper, rxPacketsUpper, txBytesUpper, txPacketsUpper)
+ } else {
+ BareStats(txBytesLower, txPacketsLower, rxBytesLower, rxPacketsLower) to
+ BareStats(txBytesUpper, txPacketsUpper, rxBytesUpper, rxPacketsUpper)
+ }
+ }
+
+ private fun genHttpTraffic(network: Network, uploadSize: Long, downloadSize: Long) =
+ genHttpTrafficWithTag(network, uploadSize, downloadSize, NetworkStats.Bucket.TAG_NONE)
+
+ private fun genHttpTrafficWithTag(
+ network: Network,
+ uploadSize: Long,
+ downloadSize: Long,
+ tag: Int
+ ) {
+ val path = "/test_upload_download"
+ val buf = ByteArray(DEFAULT_BUFFER_SIZE)
+
+ httpServer.addResponse(
+ TestHttpServer.Request(path, NanoHTTPD.Method.POST),
+ NanoHTTPD.Response.Status.OK,
+ content = getRandomString(downloadSize)
+ )
+ var httpConnection: HttpURLConnection? = null
+ try {
+ TrafficStats.setThreadStatsTag(tag)
+ val spec = "http://$HTTP_SERVER_NAME:${httpServer.listeningPort}$path"
+ val url = URL(spec)
+ httpConnection = network.openConnection(url) as HttpURLConnection
+ httpConnection.connectTimeout = CONNECTION_TIMEOUT_MILLIS
+ httpConnection.requestMethod = "POST"
+ httpConnection.doOutput = true
+ // Tell the server that the response should not be compressed. Otherwise, the data usage
+ // accounted will be less than expected.
+ httpConnection.setRequestProperty("Accept-Encoding", "identity")
+ // Tell the server that to close connection after this request, this is needed to
+ // prevent from reusing the same socket that has different tagging requirement.
+ httpConnection.setRequestProperty("Connection", "close")
+
+ // Send http body.
+ val outputStream = BufferedOutputStream(httpConnection.outputStream)
+ outputStream.write(getRandomString(uploadSize).toByteArray(Charset.forName("UTF-8")))
+ outputStream.close()
+ assertEquals(HTTP_OK, httpConnection.responseCode)
+
+ // Receive response from the server.
+ val inputStream = BufferedInputStream(httpConnection.getInputStream())
+ var total = 0L
+ while (true) {
+ val count = inputStream.read(buf)
+ if (count == -1) break // End-of-Stream
+ total += count
+ }
+ assertEquals(downloadSize, total)
+ } finally {
+ httpConnection?.inputStream?.close()
+ TrafficStats.clearThreadStatsTag()
+ }
+ }
+
+ // NetworkStats.Bucket cannot be written. So another class is needed to
+ // perform arithmetic operations.
+ data class BareStats(
+ val rxBytes: Long,
+ val rxPackets: Long,
+ val txBytes: Long,
+ val txPackets: Long
+ ) {
+ operator fun plus(other: BareStats): BareStats {
+ return BareStats(
+ this.rxBytes + other.rxBytes,
+ this.rxPackets + other.rxPackets,
+ this.txBytes + other.txBytes,
+ this.txPackets + other.txPackets
+ )
+ }
+
+ operator fun minus(other: BareStats): BareStats {
+ return BareStats(
+ this.rxBytes - other.rxBytes,
+ this.rxPackets - other.rxPackets,
+ this.txBytes - other.txBytes,
+ this.txPackets - other.txPackets
+ )
+ }
+
+ fun reverse(): BareStats =
+ BareStats(
+ rxBytes = txBytes,
+ rxPackets = txPackets,
+ txBytes = rxBytes,
+ txPackets = rxPackets
+ )
+
+ override fun toString(): String {
+ return "BareStats{rx/txBytes=$rxBytes/$txBytes, rx/txPackets=$rxPackets/$txPackets}"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is BareStats) return false
+
+ if (rxBytes != other.rxBytes) return false
+ if (rxPackets != other.rxPackets) return false
+ if (txBytes != other.txBytes) return false
+ if (txPackets != other.txPackets) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return (rxBytes * 11 + rxPackets * 13 + txBytes * 17 + txPackets * 19).toInt()
+ }
+
+ companion object {
+ val EMPTY = BareStats(0L, 0L, 0L, 0L)
+ }
+ }
+
+ data class StatsSnapshot(val context: Context, val iface: String) {
+ val statsSummary = getNetworkSummary(iface)
+ val statsUid = getUidDetail(iface, TAG_NONE)
+ val taggedSummary = getTaggedNetworkSummary(iface, TEST_TAG)
+ val taggedUid = getUidDetail(iface, TEST_TAG)
+ val trafficStatsIface = getTrafficStatsIface(iface)
+ val trafficStatsUid = getTrafficStatsUid(Process.myUid())
+
+ private fun getUidDetail(iface: String, tag: Int): BareStats {
+ return getNetworkStatsThat(iface, tag) { nsm, template ->
+ nsm.queryDetailsForUidTagState(
+ template,
+ Long.MIN_VALUE,
+ Long.MAX_VALUE,
+ Process.myUid(),
+ tag,
+ Bucket.STATE_ALL
+ )
+ }
+ }
+
+ private fun getNetworkSummary(iface: String): BareStats {
+ return getNetworkStatsThat(iface, TAG_NONE) { nsm, template ->
+ nsm.querySummary(template, Long.MIN_VALUE, Long.MAX_VALUE)
+ }
+ }
+
+ private fun getTaggedNetworkSummary(iface: String, tag: Int): BareStats {
+ return getNetworkStatsThat(iface, tag) { nsm, template ->
+ nsm.queryTaggedSummary(template, Long.MIN_VALUE, Long.MAX_VALUE)
+ }
+ }
+
+ private fun getNetworkStatsThat(
+ iface: String,
+ tag: Int,
+ queryApi: (nsm: NetworkStatsManager, template: NetworkTemplate) -> NetworkStats
+ ): BareStats {
+ val nsm = context.getSystemService(NetworkStatsManager::class.java)!!
+ nsm.forceUpdate()
+ val testTemplate = NetworkTemplate.Builder(MATCH_TEST)
+ .setWifiNetworkKeys(setOf(iface)).build()
+ val stats = queryApi.invoke(nsm, testTemplate)
+ val filteredBuckets =
+ stats.buckets().filter { it.uid == Process.myUid() && it.tag == tag }
+ return filteredBuckets.fold(BareStats.EMPTY) { acc, it ->
+ acc + BareStats(
+ it.rxBytes,
+ it.rxPackets,
+ it.txBytes,
+ it.txPackets
+ )
+ }
+ }
+
+ // Helper function to iterate buckets in app.usage.NetworkStats.
+ private fun NetworkStats.buckets() = object : Iterable<NetworkStats.Bucket> {
+ override fun iterator() = object : Iterator<NetworkStats.Bucket> {
+ override operator fun hasNext() = hasNextBucket()
+ override operator fun next() =
+ NetworkStats.Bucket().also { assertTrue(getNextBucket(it)) }
+ }
+ }
+
+ private fun getTrafficStatsIface(iface: String): BareStats = BareStats(
+ TrafficStats.getRxBytes(iface),
+ TrafficStats.getRxPackets(iface),
+ TrafficStats.getTxBytes(iface),
+ TrafficStats.getTxPackets(iface)
+ )
+
+ private fun getTrafficStatsUid(uid: Int): BareStats = BareStats(
+ TrafficStats.getUidRxBytes(uid),
+ TrafficStats.getUidRxPackets(uid),
+ TrafficStats.getUidTxBytes(uid),
+ TrafficStats.getUidTxPackets(uid)
+ )
+ }
+
+ private fun assertAllStatsIncreases(
+ before: StatsSnapshot,
+ after: StatsSnapshot,
+ lower: BareStats,
+ upper: BareStats
+ ) {
+ assertNonTaggedStatsIncreases(before, after, lower, upper)
+ assertTaggedStatsIncreases(before, after, lower, upper)
+ }
+
+ private fun assertOnlyNonTaggedStatsIncreases(
+ before: StatsSnapshot,
+ after: StatsSnapshot,
+ lower: BareStats,
+ upper: BareStats
+ ) {
+ assertNonTaggedStatsIncreases(before, after, lower, upper)
+ assertTaggedStatsEquals(before, after)
+ }
+
+ private fun assertNonTaggedStatsIncreases(
+ before: StatsSnapshot,
+ after: StatsSnapshot,
+ lower: BareStats,
+ upper: BareStats
+ ) {
+ assertInRange(
+ "Unexpected iface traffic stats",
+ after.iface,
+ before.trafficStatsIface,
+ after.trafficStatsIface,
+ lower,
+ upper
+ )
+ // Uid traffic stats are counted in both direction because the external network
+ // traffic is also attributed to the test uid.
+ assertInRange(
+ "Unexpected uid traffic stats",
+ after.iface,
+ before.trafficStatsUid,
+ after.trafficStatsUid,
+ lower + lower.reverse(),
+ upper + upper.reverse()
+ )
+ assertInRange(
+ "Unexpected non-tagged summary stats",
+ after.iface,
+ before.statsSummary,
+ after.statsSummary,
+ lower,
+ upper
+ )
+ assertInRange(
+ "Unexpected non-tagged uid stats",
+ after.iface,
+ before.statsUid,
+ after.statsUid,
+ lower,
+ upper
+ )
+ }
+
+ private fun assertTaggedStatsEquals(before: StatsSnapshot, after: StatsSnapshot) {
+ // Increment of tagged data should be zero since no tagged traffic was generated.
+ assertEquals(
+ before.taggedSummary,
+ after.taggedSummary,
+ "Unexpected tagged summary stats: ${after.iface}"
+ )
+ assertEquals(
+ before.taggedUid,
+ after.taggedUid,
+ "Unexpected tagged uid stats: ${Process.myUid()} on ${after.iface}"
+ )
+ }
+
+ private fun assertTaggedStatsIncreases(
+ before: StatsSnapshot,
+ after: StatsSnapshot,
+ lower: BareStats,
+ upper: BareStats
+ ) {
+ assertInRange(
+ "Unexpected tagged summary stats",
+ after.iface,
+ before.taggedSummary,
+ after.taggedSummary,
+ lower,
+ upper
+ )
+ assertInRange(
+ "Unexpected tagged uid stats: ${Process.myUid()}",
+ after.iface,
+ before.taggedUid,
+ after.taggedUid,
+ lower,
+ upper
+ )
+ }
+
+ /** Verify the given BareStats is in range [lower, upper] */
+ private fun assertInRange(
+ tag: String,
+ iface: String,
+ before: BareStats,
+ after: BareStats,
+ lower: BareStats,
+ upper: BareStats
+ ) {
+ // Passing the value after operation and the value before operation to dump the actual
+ // numbers if it fails.
+ assertTrue(
+ checkInRange(before, after, lower, upper),
+ "$tag on $iface: $after - $before is not within range [$lower, $upper]"
+ )
+ }
+
+ private fun checkInRange(
+ before: BareStats,
+ after: BareStats,
+ lower: BareStats,
+ upper: BareStats
+ ): Boolean {
+ val value = after - before
+ return value.rxBytes in lower.rxBytes..upper.rxBytes &&
+ value.rxPackets in lower.rxPackets..upper.rxPackets &&
+ value.txBytes in lower.txBytes..upper.txBytes &&
+ value.txPackets in lower.txPackets..upper.txPackets
+ }
+
+ fun getRandomString(length: Long): String {
+ val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
+ return (1..length)
+ .map { allowedChars.random() }
+ .joinToString("")
+ }
+}
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ServiceManagerWrapperIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ServiceManagerWrapperIntegrationTest.kt
new file mode 100644
index 0000000..7e00ed2
--- /dev/null
+++ b/tests/integration/src/com/android/server/net/integrationtests/ServiceManagerWrapperIntegrationTest.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.net.integrationtests
+
+import android.content.Context
+import android.os.Build
+import com.android.server.ServiceManagerWrapper
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Integration tests for {@link ServiceManagerWrapper}. */
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.S)
+@ConnectivityModuleTest
+class ServiceManagerWrapperIntegrationTest {
+ @Test
+ fun testWaitForService_successFullyRetrievesConnectivityServiceBinder() {
+ assertNotNull(ServiceManagerWrapper.waitForService(Context.CONNECTIVITY_SERVICE))
+ }
+}
diff --git a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
index 7e227c4..e43ce29 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
@@ -30,13 +30,14 @@
import com.android.server.NetworkStackService.NetworkStackConnector
import com.android.server.connectivity.NetworkMonitor
import com.android.server.net.integrationtests.NetworkStackInstrumentationService.InstrumentationConnector
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.spy
import java.io.ByteArrayInputStream
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
+import java.util.concurrent.ExecutorService
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
private const val TEST_NETID = 42
@@ -60,6 +61,10 @@
private class NetworkMonitorDeps(private val privateDnsBypassNetwork: Network) :
NetworkMonitor.Dependencies() {
override fun getPrivateDnsBypassNetwork(network: Network?) = privateDnsBypassNetwork
+ override fun onThreadCreated(thread: Thread) =
+ InstrumentationConnector.onNetworkMonitorThreadCreated(thread)
+ override fun onExecutorServiceCreated(ecs: ExecutorService) =
+ InstrumentationConnector.onNetworkMonitorExecutorServiceCreated(ecs)
}
/**
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/mts/Android.bp b/tests/mts/Android.bp
index 6425223..336be2e 100644
--- a/tests/mts/Android.bp
+++ b/tests/mts/Android.bp
@@ -14,6 +14,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -38,5 +39,5 @@
"bpf_existence_test.cpp",
],
compile_multilib: "first",
- min_sdk_version: "30", // Ensure test runs on R and above.
+ min_sdk_version: "30", // Ensure test runs on R and above.
}
diff --git a/tests/native/connectivity_native_test/Android.bp b/tests/native/connectivity_native_test/Android.bp
index 8825aa4..2f66d17 100644
--- a/tests/native/connectivity_native_test/Android.bp
+++ b/tests/native/connectivity_native_test/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
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/native/utilities/Android.bp b/tests/native/utilities/Android.bp
index 4706b3d..48a5414 100644
--- a/tests/native/utilities/Android.bp
+++ b/tests/native/utilities/Android.bp
@@ -14,14 +14,17 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_core_networking",
default_applicable_licenses: ["Android-Apache-2.0"],
}
+// TODO: delete this as it is a cross-module api boundary violation
cc_test_library {
name: "libconnectivity_native_test_utils",
+ visibility: ["//packages/modules/DnsResolver/tests:__subpackages__"],
defaults: [
"netd_defaults",
- "resolv_test_defaults"
+ "resolv_test_defaults",
],
srcs: [
"firewall.cpp",
diff --git a/tests/native/utilities/firewall.cpp b/tests/native/utilities/firewall.cpp
index 669b76a..34b4f07 100644
--- a/tests/native/utilities/firewall.cpp
+++ b/tests/native/utilities/firewall.cpp
@@ -60,10 +60,10 @@
// iif should be non-zero if and only if match == MATCH_IIF
if (match == IIF_MATCH && iif == 0) {
return Errorf("Interface match {} must have nonzero interface index",
- static_cast<int>(match));
+ static_cast<uint32_t>(match));
} else if (match != IIF_MATCH && iif != 0) {
return Errorf("Non-interface match {} must have zero interface index",
- static_cast<int>(match));
+ static_cast<uint32_t>(match));
}
std::lock_guard guard(mMutex);
@@ -71,14 +71,14 @@
if (oldMatch.ok()) {
UidOwnerValue newMatch = {
.iif = iif ? iif : oldMatch.value().iif,
- .rule = static_cast<uint8_t>(oldMatch.value().rule | match),
+ .rule = oldMatch.value().rule | match,
};
auto res = mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY);
if (!res.ok()) return Errorf("Failed to update rule: {}", res.error().message());
} else {
UidOwnerValue newMatch = {
.iif = iif,
- .rule = static_cast<uint8_t>(match),
+ .rule = match,
};
auto res = mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY);
if (!res.ok()) return Errorf("Failed to add rule: {}", res.error().message());
@@ -93,7 +93,7 @@
UidOwnerValue newMatch = {
.iif = (match == IIF_MATCH) ? 0 : oldMatch.value().iif,
- .rule = static_cast<uint8_t>(oldMatch.value().rule & ~match),
+ .rule = oldMatch.value().rule & ~match,
};
if (newMatch.rule == 0) {
auto res = mUidOwnerMap.deleteValue(uid);
diff --git a/tests/smoketest/Android.bp b/tests/smoketest/Android.bp
index 4ab24fc..121efa1 100644
--- a/tests/smoketest/Android.bp
+++ b/tests/smoketest/Android.bp
@@ -10,6 +10,7 @@
// TODO: remove this hack when there is a better solution for jni_libs that includes
// dependent libraries.
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 8b286a0..2f88c41 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -2,6 +2,7 @@
// Build FrameworksNetTests package
//########################################################################
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "Android-Apache-2.0"
@@ -57,34 +58,18 @@
filegroup {
name: "non-connectivity-module-test",
srcs: [
- "java/android/net/Ikev2VpnProfileTest.java",
"java/android/net/IpMemoryStoreTest.java",
"java/android/net/TelephonyNetworkSpecifierTest.java",
- "java/android/net/VpnManagerTest.java",
"java/android/net/ipmemorystore/*.java",
"java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt",
"java/com/android/internal/net/NetworkUtilsInternalTest.java",
- "java/com/android/internal/net/VpnProfileTest.java",
- "java/com/android/server/VpnManagerServiceTest.java",
"java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java",
"java/com/android/server/connectivity/IpConnectivityMetricsTest.java",
"java/com/android/server/connectivity/MetricsTestUtil.java",
"java/com/android/server/connectivity/MultipathPolicyTrackerTest.java",
"java/com/android/server/connectivity/NetdEventListenerServiceTest.java",
- "java/com/android/server/connectivity/VpnTest.java",
"java/com/android/server/net/ipmemorystore/*.java",
- ]
-}
-
-// Subset of services-core used to by ConnectivityService tests to test VPN realistically.
-// This is stripped by jarjar (see rules below) from other unrelated classes, so tests do not
-// include most classes from services-core, which are unrelated and cause wrong code coverage
-// calculations.
-java_library {
- name: "services.core-vpn",
- static_libs: ["services.core"],
- jarjar_rules: "vpn-jarjar-rules.txt",
- visibility: ["//visibility:private"],
+ ],
}
java_defaults {
@@ -113,9 +98,8 @@
"platform-test-annotations",
"service-connectivity-pre-jarjar",
"service-connectivity-tiramisu-pre-jarjar",
- "services.core-vpn",
"testables",
- "cts-net-utils"
+ "cts-net-utils",
],
libs: [
"android.net.ipsec.ike.stubs.module_lib",
diff --git a/tests/unit/java/android/net/Ikev2VpnProfileTest.java b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
deleted file mode 100644
index e12e961..0000000
--- a/tests/unit/java/android/net/Ikev2VpnProfileTest.java
+++ /dev/null
@@ -1,585 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net;
-
-import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V6;
-import static android.net.cts.util.IkeSessionTestUtils.getTestIkeSessionParams;
-
-import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
-
-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;
-
-import android.net.ipsec.ike.IkeKeyIdIdentification;
-import android.net.ipsec.ike.IkeTunnelConnectionParams;
-import android.os.Build;
-import android.test.mock.MockContext;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.net.VpnProfile;
-import com.android.internal.org.bouncycastle.x509.X509V1CertificateGenerator;
-import com.android.net.module.util.ProxyUtils;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.math.BigInteger;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.PrivateKey;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import javax.security.auth.x500.X500Principal;
-
-/** Unit tests for {@link Ikev2VpnProfile.Builder}. */
-@SmallTest
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-public class Ikev2VpnProfileTest {
- private static final String SERVER_ADDR_STRING = "1.2.3.4";
- private static final String IDENTITY_STRING = "Identity";
- private static final String USERNAME_STRING = "username";
- private static final String PASSWORD_STRING = "pa55w0rd";
- private static final String EXCL_LIST = "exclList";
- private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
- private static final int TEST_MTU = 1300;
-
- @Rule
- public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
-
- private final MockContext mMockContext =
- new MockContext() {
- @Override
- public String getOpPackageName() {
- return "fooPackage";
- }
- };
- private final ProxyInfo mProxy = ProxyInfo.buildDirectProxy(
- SERVER_ADDR_STRING, -1, ProxyUtils.exclusionStringAsList(EXCL_LIST));
-
- private X509Certificate mUserCert;
- private X509Certificate mServerRootCa;
- private PrivateKey mPrivateKey;
-
- @Before
- public void setUp() throws Exception {
- mServerRootCa = generateRandomCertAndKeyPair().cert;
-
- final CertificateAndKey userCertKey = generateRandomCertAndKeyPair();
- mUserCert = userCertKey.cert;
- mPrivateKey = userCertKey.key;
- }
-
- private Ikev2VpnProfile.Builder getBuilderWithDefaultOptions() {
- final Ikev2VpnProfile.Builder builder =
- new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING);
-
- builder.setBypassable(true);
- builder.setProxy(mProxy);
- builder.setMaxMtu(TEST_MTU);
- builder.setMetered(true);
-
- return builder;
- }
-
- @Test
- public void testBuildValidProfileWithOptions() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
- final Ikev2VpnProfile profile = builder.build();
- assertNotNull(profile);
-
- // Check non-auth parameters correctly stored
- assertEquals(SERVER_ADDR_STRING, profile.getServerAddr());
- assertEquals(IDENTITY_STRING, profile.getUserIdentity());
- assertEquals(mProxy, profile.getProxyInfo());
- assertTrue(profile.isBypassable());
- assertTrue(profile.isMetered());
- assertEquals(TEST_MTU, profile.getMaxMtu());
- assertEquals(Ikev2VpnProfile.DEFAULT_ALGORITHMS, profile.getAllowedAlgorithms());
- }
-
- @Test
- public void testBuildUsernamePasswordProfile() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
- final Ikev2VpnProfile profile = builder.build();
- assertNotNull(profile);
-
- assertEquals(USERNAME_STRING, profile.getUsername());
- assertEquals(PASSWORD_STRING, profile.getPassword());
- assertEquals(mServerRootCa, profile.getServerRootCaCert());
-
- assertNull(profile.getPresharedKey());
- assertNull(profile.getRsaPrivateKey());
- assertNull(profile.getUserCert());
- }
-
- @Test
- public void testBuildDigitalSignatureProfile() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
- final Ikev2VpnProfile profile = builder.build();
- assertNotNull(profile);
-
- assertEquals(profile.getUserCert(), mUserCert);
- assertEquals(mPrivateKey, profile.getRsaPrivateKey());
- assertEquals(profile.getServerRootCaCert(), mServerRootCa);
-
- assertNull(profile.getPresharedKey());
- assertNull(profile.getUsername());
- assertNull(profile.getPassword());
- }
-
- @Test
- public void testBuildPresharedKeyProfile() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- builder.setAuthPsk(PSK_BYTES);
- final Ikev2VpnProfile profile = builder.build();
- assertNotNull(profile);
-
- assertArrayEquals(PSK_BYTES, profile.getPresharedKey());
-
- assertNull(profile.getServerRootCaCert());
- assertNull(profile.getUsername());
- assertNull(profile.getPassword());
- assertNull(profile.getRsaPrivateKey());
- assertNull(profile.getUserCert());
- }
-
- @Test
- public void testBuildWithAllowedAlgorithmsAead() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
- builder.setAuthPsk(PSK_BYTES);
-
- List<String> allowedAlgorithms =
- Arrays.asList(
- IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
- IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305);
- builder.setAllowedAlgorithms(allowedAlgorithms);
-
- final Ikev2VpnProfile profile = builder.build();
- assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms());
- }
-
- @Test
- public void testBuildWithAllowedAlgorithmsNormal() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
- builder.setAuthPsk(PSK_BYTES);
-
- List<String> allowedAlgorithms =
- Arrays.asList(
- IpSecAlgorithm.AUTH_HMAC_SHA512,
- IpSecAlgorithm.AUTH_AES_XCBC,
- IpSecAlgorithm.AUTH_AES_CMAC,
- IpSecAlgorithm.CRYPT_AES_CBC,
- IpSecAlgorithm.CRYPT_AES_CTR);
- builder.setAllowedAlgorithms(allowedAlgorithms);
-
- final Ikev2VpnProfile profile = builder.build();
- assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms());
- }
-
- @Test
- public void testSetAllowedAlgorithmsEmptyList() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- try {
- builder.setAllowedAlgorithms(new ArrayList<>());
- fail("Expected exception due to no valid algorithm set");
- } catch (IllegalArgumentException expected) {
- }
- }
-
- @Test
- public void testSetAllowedAlgorithmsInvalidList() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
- List<String> allowedAlgorithms = new ArrayList<>();
-
- try {
- builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA256));
- fail("Expected exception due to missing encryption");
- } catch (IllegalArgumentException expected) {
- }
-
- try {
- builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.CRYPT_AES_CBC));
- fail("Expected exception due to missing authentication");
- } catch (IllegalArgumentException expected) {
- }
- }
-
- @Test
- public void testSetAllowedAlgorithmsInsecureAlgorithm() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
- List<String> allowedAlgorithms = new ArrayList<>();
-
- try {
- builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_MD5));
- fail("Expected exception due to insecure algorithm");
- } catch (IllegalArgumentException expected) {
- }
-
- try {
- builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA1));
- fail("Expected exception due to insecure algorithm");
- } catch (IllegalArgumentException expected) {
- }
- }
-
- @Test
- public void testBuildNoAuthMethodSet() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- try {
- builder.build();
- fail("Expected exception due to lack of auth method");
- } catch (IllegalArgumentException expected) {
- }
- }
-
-
- // TODO: Refer to Build.VERSION_CODES.SC_V2 when it's available in AOSP and mainline branch
- @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
- @Test
- public void testBuildExcludeLocalRoutesSet() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
- builder.setAuthPsk(PSK_BYTES);
- builder.setLocalRoutesExcluded(true);
-
- final Ikev2VpnProfile profile = builder.build();
- assertNotNull(profile);
- assertTrue(profile.areLocalRoutesExcluded());
-
- builder.setBypassable(false);
- try {
- builder.build();
- fail("Expected exception because excludeLocalRoutes should be set only"
- + " on the bypassable VPN");
- } catch (IllegalArgumentException expected) {
- }
- }
-
- @Test
- public void testBuildInvalidMtu() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- try {
- builder.setMaxMtu(500);
- fail("Expected exception due to too-small MTU");
- } catch (IllegalArgumentException expected) {
- }
- }
-
- private void verifyVpnProfileCommon(VpnProfile profile) {
- assertEquals(SERVER_ADDR_STRING, profile.server);
- assertEquals(IDENTITY_STRING, profile.ipsecIdentifier);
- assertEquals(mProxy, profile.proxy);
- assertTrue(profile.isBypassable);
- assertTrue(profile.isMetered);
- assertEquals(TEST_MTU, profile.maxMtu);
- }
-
- @Test
- public void testPskConvertToVpnProfile() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- builder.setAuthPsk(PSK_BYTES);
- final VpnProfile profile = builder.build().toVpnProfile();
-
- verifyVpnProfileCommon(profile);
- assertEquals(Ikev2VpnProfile.encodeForIpsecSecret(PSK_BYTES), profile.ipsecSecret);
-
- // Check nothing else is set
- assertEquals("", profile.username);
- assertEquals("", profile.password);
- assertEquals("", profile.ipsecUserCert);
- assertEquals("", profile.ipsecCaCert);
- }
-
- @Test
- public void testUsernamePasswordConvertToVpnProfile() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
- final VpnProfile profile = builder.build().toVpnProfile();
-
- verifyVpnProfileCommon(profile);
- assertEquals(USERNAME_STRING, profile.username);
- assertEquals(PASSWORD_STRING, profile.password);
- assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);
-
- // Check nothing else is set
- assertEquals("", profile.ipsecUserCert);
- assertEquals("", profile.ipsecSecret);
- }
-
- @Test
- public void testRsaConvertToVpnProfile() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
- final VpnProfile profile = builder.build().toVpnProfile();
-
- final String expectedSecret = Ikev2VpnProfile.PREFIX_INLINE
- + Ikev2VpnProfile.encodeForIpsecSecret(mPrivateKey.getEncoded());
- verifyVpnProfileCommon(profile);
- assertEquals(Ikev2VpnProfile.certificateToPemString(mUserCert), profile.ipsecUserCert);
- assertEquals(
- expectedSecret,
- profile.ipsecSecret);
- assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);
-
- // Check nothing else is set
- assertEquals("", profile.username);
- assertEquals("", profile.password);
- }
-
- @Test
- public void testPskFromVpnProfileDiscardsIrrelevantValues() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- builder.setAuthPsk(PSK_BYTES);
- final VpnProfile profile = builder.build().toVpnProfile();
- profile.username = USERNAME_STRING;
- profile.password = PASSWORD_STRING;
- profile.ipsecCaCert = Ikev2VpnProfile.certificateToPemString(mServerRootCa);
- profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert);
-
- final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
- assertNull(result.getUsername());
- assertNull(result.getPassword());
- assertNull(result.getUserCert());
- assertNull(result.getRsaPrivateKey());
- assertNull(result.getServerRootCaCert());
- }
-
- @Test
- public void testUsernamePasswordFromVpnProfileDiscardsIrrelevantValues() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
- final VpnProfile profile = builder.build().toVpnProfile();
- profile.ipsecSecret = new String(PSK_BYTES);
- profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert);
-
- final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
- assertNull(result.getPresharedKey());
- assertNull(result.getUserCert());
- assertNull(result.getRsaPrivateKey());
- }
-
- @Test
- public void testRsaFromVpnProfileDiscardsIrrelevantValues() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
- final VpnProfile profile = builder.build().toVpnProfile();
- profile.username = USERNAME_STRING;
- profile.password = PASSWORD_STRING;
-
- final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
- assertNull(result.getUsername());
- assertNull(result.getPassword());
- assertNull(result.getPresharedKey());
- }
-
- @Test
- public void testPskConversionIsLossless() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- builder.setAuthPsk(PSK_BYTES);
- final Ikev2VpnProfile ikeProfile = builder.build();
-
- assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
- }
-
- @Test
- public void testUsernamePasswordConversionIsLossless() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
- final Ikev2VpnProfile ikeProfile = builder.build();
-
- assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
- }
-
- @Test
- public void testRsaConversionIsLossless() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
- final Ikev2VpnProfile ikeProfile = builder.build();
-
- assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
- }
-
- @Test
- public void testBuildWithIkeTunConnParamsConvertToVpnProfile() throws Exception {
- // Special keyId that contains delimiter character of VpnProfile
- final byte[] keyId = "foo\0bar".getBytes();
- final IkeTunnelConnectionParams tunnelParams = new IkeTunnelConnectionParams(
- getTestIkeSessionParams(true /* testIpv6 */, new IkeKeyIdIdentification(keyId)),
- CHILD_PARAMS);
- final Ikev2VpnProfile ikev2VpnProfile = new Ikev2VpnProfile.Builder(tunnelParams).build();
- final VpnProfile vpnProfile = ikev2VpnProfile.toVpnProfile();
-
- assertEquals(VpnProfile.TYPE_IKEV2_FROM_IKE_TUN_CONN_PARAMS, vpnProfile.type);
-
- // Username, password, server, ipsecIdentifier, ipsecCaCert, ipsecSecret, ipsecUserCert and
- // getAllowedAlgorithms should not be set if IkeTunnelConnectionParams is set.
- assertEquals("", vpnProfile.server);
- assertEquals("", vpnProfile.ipsecIdentifier);
- assertEquals("", vpnProfile.username);
- assertEquals("", vpnProfile.password);
- assertEquals("", vpnProfile.ipsecCaCert);
- assertEquals("", vpnProfile.ipsecSecret);
- assertEquals("", vpnProfile.ipsecUserCert);
- assertEquals(0, vpnProfile.getAllowedAlgorithms().size());
-
- // IkeTunnelConnectionParams should stay the same.
- assertEquals(tunnelParams, vpnProfile.ikeTunConnParams);
-
- // Convert to disk-stable format and then back to Ikev2VpnProfile should be the same.
- final VpnProfile decodedVpnProfile =
- VpnProfile.decode(vpnProfile.key, vpnProfile.encode());
- final Ikev2VpnProfile convertedIkev2VpnProfile =
- Ikev2VpnProfile.fromVpnProfile(decodedVpnProfile);
- assertEquals(ikev2VpnProfile, convertedIkev2VpnProfile);
- }
-
- @Test
- public void testConversionIsLosslessWithIkeTunConnParams() throws Exception {
- final IkeTunnelConnectionParams tunnelParams =
- new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
- // Config authentication related fields is not required while building with
- // IkeTunnelConnectionParams.
- final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams).build();
- assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
- }
-
- @Test
- public void testAutomaticNattAndIpVersionConversionIsLossless() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
- builder.setAutomaticNattKeepaliveTimerEnabled(true);
- builder.setAutomaticIpVersionSelectionEnabled(true);
-
- builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
- final Ikev2VpnProfile ikeProfile = builder.build();
-
- assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
- }
-
- @Test
- public void testAutomaticNattAndIpVersionDefaults() throws Exception {
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
-
- builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
- final Ikev2VpnProfile ikeProfile = builder.build();
-
- assertEquals(false, ikeProfile.isAutomaticNattKeepaliveTimerEnabled());
- assertEquals(false, ikeProfile.isAutomaticIpVersionSelectionEnabled());
- }
-
- @Test
- public void testEquals() throws Exception {
- // Verify building without IkeTunnelConnectionParams
- final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
- builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
- assertEquals(builder.build(), builder.build());
-
- // Verify building with IkeTunnelConnectionParams
- final IkeTunnelConnectionParams tunnelParams =
- new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
- final IkeTunnelConnectionParams tunnelParams2 =
- new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
- assertEquals(new Ikev2VpnProfile.Builder(tunnelParams).build(),
- new Ikev2VpnProfile.Builder(tunnelParams2).build());
- }
-
- @Test
- public void testBuildProfileWithNullProxy() throws Exception {
- final Ikev2VpnProfile ikev2VpnProfile =
- new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING)
- .setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa)
- .build();
-
- // ProxyInfo should be null for the profile without setting ProxyInfo.
- assertNull(ikev2VpnProfile.getProxyInfo());
-
- // ProxyInfo should stay null after performing toVpnProfile() and fromVpnProfile()
- final VpnProfile vpnProfile = ikev2VpnProfile.toVpnProfile();
- assertNull(vpnProfile.proxy);
-
- final Ikev2VpnProfile convertedIkev2VpnProfile = Ikev2VpnProfile.fromVpnProfile(vpnProfile);
- assertNull(convertedIkev2VpnProfile.getProxyInfo());
- }
-
- private static class CertificateAndKey {
- public final X509Certificate cert;
- public final PrivateKey key;
-
- CertificateAndKey(X509Certificate cert, PrivateKey key) {
- this.cert = cert;
- this.key = key;
- }
- }
-
- private static CertificateAndKey generateRandomCertAndKeyPair() throws Exception {
- final Date validityBeginDate =
- new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1L));
- final Date validityEndDate =
- new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1L));
-
- // Generate a keypair
- final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
- keyPairGenerator.initialize(512);
- final KeyPair keyPair = keyPairGenerator.generateKeyPair();
-
- final X500Principal dnName = new X500Principal("CN=test.android.com");
- final X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
- certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
- certGen.setSubjectDN(dnName);
- certGen.setIssuerDN(dnName);
- certGen.setNotBefore(validityBeginDate);
- certGen.setNotAfter(validityEndDate);
- certGen.setPublicKey(keyPair.getPublic());
- certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
-
- final X509Certificate cert = certGen.generate(keyPair.getPrivate(), "AndroidOpenSSL");
- return new CertificateAndKey(cert, keyPair.getPrivate());
- }
-}
diff --git a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt b/tests/unit/java/android/net/NetworkStackBpfNetMapsTest.kt
similarity index 83%
rename from tests/unit/java/android/net/BpfNetMapsReaderTest.kt
rename to tests/unit/java/android/net/NetworkStackBpfNetMapsTest.kt
index 8919666..a9ccbdd 100644
--- a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
+++ b/tests/unit/java/android/net/NetworkStackBpfNetMapsTest.kt
@@ -26,6 +26,7 @@
import android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY
import android.net.BpfNetMapsUtils.getMatchByFirewallChain
import android.os.Build.VERSION_CODES
+import android.os.Process.FIRST_APPLICATION_UID
import com.android.net.module.util.IBpfMap
import com.android.net.module.util.Struct.S32
import com.android.net.module.util.Struct.U32
@@ -42,7 +43,7 @@
import org.junit.Test
import org.junit.runner.RunWith
-private const val TEST_UID1 = 1234
+private const val TEST_UID1 = 11234
private const val TEST_UID2 = TEST_UID1 + 1
private const val TEST_UID3 = TEST_UID2 + 1
private const val NO_IIF = 0
@@ -50,7 +51,7 @@
// pre-T devices does not support Bpf.
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(VERSION_CODES.S_V2)
-class BpfNetMapsReaderTest {
+class NetworkStackBpfNetMapsTest {
@Rule
@JvmField
val ignoreRule = DevSdkIgnoreRule()
@@ -58,14 +59,15 @@
private val testConfigurationMap: IBpfMap<S32, U32> = TestBpfMap()
private val testUidOwnerMap: IBpfMap<S32, UidOwnerValue> = TestBpfMap()
private val testDataSaverEnabledMap: IBpfMap<S32, U8> = TestBpfMap()
- private val bpfNetMapsReader = BpfNetMapsReader(
- TestDependencies(testConfigurationMap, testUidOwnerMap, testDataSaverEnabledMap))
+ private val bpfNetMapsReader = NetworkStackBpfNetMaps(
+ TestDependencies(testConfigurationMap, testUidOwnerMap, testDataSaverEnabledMap)
+ )
class TestDependencies(
private val configMap: IBpfMap<S32, U32>,
private val uidOwnerMap: IBpfMap<S32, UidOwnerValue>,
private val dataSaverEnabledMap: IBpfMap<S32, U8>
- ) : BpfNetMapsReader.Dependencies() {
+ ) : NetworkStackBpfNetMaps.Dependencies() {
override fun getConfigurationMap() = configMap
override fun getUidOwnerMap() = uidOwnerMap
override fun getDataSaverEnabledMap() = dataSaverEnabledMap
@@ -99,11 +101,16 @@
Modifier.isStatic(it.modifiers) && it.name.startsWith("FIREWALL_CHAIN_")
}
// Verify the size matches, this also verifies no common item in allow and deny chains.
- assertEquals(BpfNetMapsConstants.ALLOW_CHAINS.size +
- BpfNetMapsConstants.DENY_CHAINS.size, declaredChains.size)
+ assertEquals(
+ BpfNetMapsConstants.ALLOW_CHAINS.size +
+ BpfNetMapsConstants.DENY_CHAINS.size,
+ declaredChains.size
+ )
declaredChains.forEach {
- assertTrue(BpfNetMapsConstants.ALLOW_CHAINS.contains(it.get(null)) ||
- BpfNetMapsConstants.DENY_CHAINS.contains(it.get(null)))
+ assertTrue(
+ BpfNetMapsConstants.ALLOW_CHAINS.contains(it.get(null)) ||
+ BpfNetMapsConstants.DENY_CHAINS.contains(it.get(null))
+ )
}
}
@@ -117,11 +124,17 @@
testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(newConfig))
}
- fun isUidNetworkingBlocked(uid: Int, metered: Boolean = false, dataSaver: Boolean = false) =
- bpfNetMapsReader.isUidNetworkingBlocked(uid, metered, dataSaver)
+ private fun mockDataSaverEnabled(enabled: Boolean) {
+ val dataSaverValue = if (enabled) {DATA_SAVER_ENABLED} else {DATA_SAVER_DISABLED}
+ testDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, U8(dataSaverValue))
+ }
+
+ fun isUidNetworkingBlocked(uid: Int, metered: Boolean = false) =
+ bpfNetMapsReader.isUidNetworkingBlocked(uid, metered)
@Test
fun testIsUidNetworkingBlockedByFirewallChains_allowChain() {
+ mockDataSaverEnabled(enabled = false)
// With everything disabled by default, verify the return value is false.
testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
assertFalse(isUidNetworkingBlocked(TEST_UID1))
@@ -141,6 +154,7 @@
@Test
fun testIsUidNetworkingBlockedByFirewallChains_denyChain() {
+ mockDataSaverEnabled(enabled = false)
// Enable standby chain but does not provide denied list. Verify the network is allowed
// for all uids.
testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
@@ -162,12 +176,14 @@
testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_POWERSAVE, true)
mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_STANDBY, true)
+ mockDataSaverEnabled(enabled = false)
assertTrue(isUidNetworkingBlocked(TEST_UID1))
}
@IgnoreUpTo(VERSION_CODES.S_V2)
@Test
fun testIsUidNetworkingBlockedByDataSaver() {
+ mockDataSaverEnabled(enabled = false)
// With everything disabled by default, verify the return value is false.
testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
assertFalse(isUidNetworkingBlocked(TEST_UID1, metered = true))
@@ -180,10 +196,11 @@
// Enable data saver, verify the network is blocked for uid1, uid2, but uid3 in happy box
// is not affected.
+ mockDataSaverEnabled(enabled = true)
testUidOwnerMap.updateEntry(S32(TEST_UID3), UidOwnerValue(NO_IIF, HAPPY_BOX_MATCH))
- assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true, dataSaver = true))
- assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true, dataSaver = true))
- assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true, dataSaver = true))
+ assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+ assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true))
+ assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true))
// Add uid1 to happy box as well, verify nothing is changed because penalty box has higher
// priority.
@@ -191,18 +208,19 @@
S32(TEST_UID1),
UidOwnerValue(NO_IIF, PENALTY_BOX_MATCH or HAPPY_BOX_MATCH)
)
- assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true, dataSaver = true))
- assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true, dataSaver = true))
- assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true, dataSaver = true))
+ assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+ assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true))
+ assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true))
// Enable doze mode, verify uid3 is blocked even if it is in happy box.
mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE, true)
- assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true, dataSaver = true))
- assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true, dataSaver = true))
- assertTrue(isUidNetworkingBlocked(TEST_UID3, metered = true, dataSaver = true))
+ assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+ assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true))
+ assertTrue(isUidNetworkingBlocked(TEST_UID3, metered = true))
// Disable doze mode and data saver, only uid1 which is in penalty box is blocked.
mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE, false)
+ mockDataSaverEnabled(enabled = false)
assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
assertFalse(isUidNetworkingBlocked(TEST_UID2, metered = true))
assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true))
@@ -214,6 +232,24 @@
}
@Test
+ fun testIsUidNetworkingBlocked_SystemUid() {
+ mockDataSaverEnabled(enabled = false)
+ testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
+ mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE, true)
+
+ for (uid in FIRST_APPLICATION_UID - 5..FIRST_APPLICATION_UID + 5) {
+ // system uid is not blocked regardless of firewall chains
+ val expectBlocked = uid >= FIRST_APPLICATION_UID
+ testUidOwnerMap.updateEntry(S32(uid), UidOwnerValue(NO_IIF, PENALTY_BOX_MATCH))
+ assertEquals(
+ expectBlocked,
+ isUidNetworkingBlocked(uid, metered = true),
+ "isUidNetworkingBlocked returns unexpected value for uid = " + uid
+ )
+ }
+ }
+
+ @Test
fun testGetDataSaverEnabled() {
testDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, U8(DATA_SAVER_DISABLED))
assertFalse(bpfNetMapsReader.dataSaverEnabled)
diff --git a/tests/unit/java/android/net/NetworkUtilsTest.java b/tests/unit/java/android/net/NetworkUtilsTest.java
index 5d789b4..e453c02 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;
@@ -154,10 +159,9 @@
return timeval;
}
- @Test
- public void testSetSockOptBytes() throws ErrnoException {
- final FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
- final StructTimeval writeTimeval = StructTimeval.fromMillis(1200);
+ private void testSetSockOptBytes(FileDescriptor sock, long timeValMillis)
+ throws ErrnoException {
+ final StructTimeval writeTimeval = StructTimeval.fromMillis(timeValMillis);
byte[] timeval = getTimevalBytes(writeTimeval);
final StructTimeval readTimeval;
@@ -165,6 +169,22 @@
readTimeval = Os.getsockoptTimeval(sock, SOL_SOCKET, SO_RCVTIMEO);
assertEquals(writeTimeval, readTimeval);
+ }
+
+ @Test
+ public void testSetSockOptBytes() throws ErrnoException {
+ final FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
+
+ testSetSockOptBytes(sock, 3000);
+
+ testSetSockOptBytes(sock, 5000);
+
SocketUtils.closeSocketQuietly(sock);
}
+
+ @Test
+ public void testIsKernel64Bit() {
+ assumeTrue(getVsrApiLevel() > Build.VERSION_CODES.TIRAMISU);
+ assertTrue(NetworkUtils.isKernel64Bit());
+ }
}
diff --git a/tests/unit/java/android/net/VpnManagerTest.java b/tests/unit/java/android/net/VpnManagerTest.java
deleted file mode 100644
index 2ab4e45..0000000
--- a/tests/unit/java/android/net/VpnManagerTest.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assume.assumeFalse;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.os.Build;
-import android.test.mock.MockContext;
-import android.util.SparseArray;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.InstrumentationRegistry;
-
-import com.android.internal.net.VpnProfile;
-import com.android.internal.util.MessageUtils;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/** Unit tests for {@link VpnManager}. */
-@SmallTest
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-public class VpnManagerTest {
-
- private static final String PKG_NAME = "fooPackage";
-
- private static final String SESSION_NAME_STRING = "testSession";
- private static final String SERVER_ADDR_STRING = "1.2.3.4";
- private static final String IDENTITY_STRING = "Identity";
- private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
-
- private IVpnManager mMockService;
- private VpnManager mVpnManager;
- private final MockContext mMockContext =
- new MockContext() {
- @Override
- public String getOpPackageName() {
- return PKG_NAME;
- }
- };
-
- @Before
- public void setUp() throws Exception {
- assumeFalse("Skipping test because watches don't support VPN",
- InstrumentationRegistry.getContext().getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_WATCH));
- mMockService = mock(IVpnManager.class);
- mVpnManager = new VpnManager(mMockContext, mMockService);
- }
-
- @Test
- public void testProvisionVpnProfilePreconsented() throws Exception {
- final PlatformVpnProfile profile = getPlatformVpnProfile();
- when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME)))
- .thenReturn(true);
-
- // Expect there to be no intent returned, as consent has already been granted.
- assertNull(mVpnManager.provisionVpnProfile(profile));
- verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME));
- }
-
- @Test
- public void testProvisionVpnProfileNeedsConsent() throws Exception {
- final PlatformVpnProfile profile = getPlatformVpnProfile();
- when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME)))
- .thenReturn(false);
-
- // Expect intent to be returned, as consent has not already been granted.
- final Intent intent = mVpnManager.provisionVpnProfile(profile);
- assertNotNull(intent);
-
- final ComponentName expectedComponentName =
- ComponentName.unflattenFromString(
- "com.android.vpndialogs/com.android.vpndialogs.PlatformVpnConfirmDialog");
- assertEquals(expectedComponentName, intent.getComponent());
- verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME));
- }
-
- @Test
- public void testDeleteProvisionedVpnProfile() throws Exception {
- mVpnManager.deleteProvisionedVpnProfile();
- verify(mMockService).deleteVpnProfile(eq(PKG_NAME));
- }
-
- @Test
- public void testStartProvisionedVpnProfile() throws Exception {
- mVpnManager.startProvisionedVpnProfile();
- verify(mMockService).startVpnProfile(eq(PKG_NAME));
- }
-
- @Test
- public void testStopProvisionedVpnProfile() throws Exception {
- mVpnManager.stopProvisionedVpnProfile();
- verify(mMockService).stopVpnProfile(eq(PKG_NAME));
- }
-
- private Ikev2VpnProfile getPlatformVpnProfile() throws Exception {
- return new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING)
- .setBypassable(true)
- .setMaxMtu(1300)
- .setMetered(true)
- .setAuthPsk(PSK_BYTES)
- .build();
- }
-
- @Test
- public void testVpnTypesEqual() throws Exception {
- SparseArray<String> vmVpnTypes = MessageUtils.findMessageNames(
- new Class[] { VpnManager.class }, new String[]{ "TYPE_VPN_" });
- SparseArray<String> nativeVpnType = MessageUtils.findMessageNames(
- new Class[] { NativeVpnType.class }, new String[]{ "" });
-
- // TYPE_VPN_NONE = -1 is only defined in VpnManager.
- assertEquals(vmVpnTypes.size() - 1, nativeVpnType.size());
- for (int i = VpnManager.TYPE_VPN_SERVICE; i < vmVpnTypes.size(); i++) {
- assertEquals(vmVpnTypes.get(i), "TYPE_VPN_" + nativeVpnType.get(i));
- }
- }
-}
diff --git a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
new file mode 100644
index 0000000..c491f37
--- /dev/null
+++ b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd
+
+import android.net.nsd.AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY
+import android.net.nsd.NsdManager.PROTOCOL_DNS_SD
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.parcelingRoundTrip
+import java.time.Duration
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// TODO: move this class to CTS tests when AdvertisingRequest is made public
+/** Unit tests for {@link AdvertisingRequest}. */
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@ConnectivityModuleTest
+class AdvertisingRequestTest {
+ @Test
+ fun testParcelingIsLossLess() {
+ val info = NsdServiceInfo().apply {
+ serviceType = "_ipp._tcp"
+ }
+ val beforeParcel = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+ .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setTtl(Duration.ofSeconds(30L))
+ .build()
+
+ val afterParcel = parcelingRoundTrip(beforeParcel)
+
+ assertEquals(beforeParcel.serviceInfo.serviceType, afterParcel.serviceInfo.serviceType)
+ assertEquals(beforeParcel.advertisingConfig, afterParcel.advertisingConfig)
+ }
+
+ @Test
+ fun testBuilder_setNullTtl_success() {
+ val info = NsdServiceInfo().apply {
+ serviceType = "_ipp._tcp"
+ }
+ val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+ .setTtl(null)
+ .build()
+
+ assertNull(request.ttl)
+ }
+
+ @Test
+ fun testBuilder_setPropertiesSuccess() {
+ val info = NsdServiceInfo().apply {
+ serviceType = "_ipp._tcp"
+ }
+ val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+ .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setTtl(Duration.ofSeconds(100L))
+ .build()
+
+ assertEquals("_ipp._tcp", request.serviceInfo.serviceType)
+ assertEquals(PROTOCOL_DNS_SD, request.protocolType)
+ assertEquals(NSD_ADVERTISING_UPDATE_ONLY, request.advertisingConfig)
+ assertEquals(Duration.ofSeconds(100L), request.ttl)
+ }
+
+ @Test
+ fun testEquality() {
+ val info = NsdServiceInfo().apply {
+ serviceType = "_ipp._tcp"
+ }
+ val request1 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
+ val request2 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
+ val request3 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+ .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setTtl(Duration.ofSeconds(120L))
+ .build()
+ val request4 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+ .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setTtl(Duration.ofSeconds(120L))
+ .build()
+
+ assertEquals(request1, request2)
+ assertEquals(request3, request4)
+ assertNotEquals(request1, request3)
+ assertNotEquals(request2, request4)
+ }
+}
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 461ead8..27c4561 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -23,6 +23,7 @@
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -52,6 +53,10 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.net.InetAddress;
+import java.util.List;
+import java.time.Duration;
+
@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@@ -221,6 +226,23 @@
verify(listener, timeout(mTimeoutMs).times(1)).onServiceRegistered(request);
}
+ @Test
+ public void testRegisterServiceWithCustomTtl() throws Exception {
+ final NsdManager manager = mManager;
+ final NsdServiceInfo info = new NsdServiceInfo("another_name2", "another_type2");
+ info.setPort(2203);
+ final AdvertisingRequest request = new AdvertisingRequest.Builder(info, PROTOCOL)
+ .setTtl(Duration.ofSeconds(30)).build();
+ final NsdManager.RegistrationListener listener = mock(
+ NsdManager.RegistrationListener.class);
+
+ manager.registerService(request, Runnable::run, listener);
+
+ AdvertisingRequest capturedRequest = getAdvertisingRequest(
+ req -> verify(mServiceConn).registerService(anyInt(), req.capture()));
+ assertEquals(request.getTtl(), capturedRequest.getTtl());
+ }
+
private void doTestRegisterService() throws Exception {
NsdManager manager = mManager;
@@ -285,6 +307,7 @@
private void doTestDiscoverService() throws Exception {
NsdManager manager = mManager;
+ DiscoveryRequest request1 = new DiscoveryRequest.Builder("a_type").build();
NsdServiceInfo reply1 = new NsdServiceInfo("a_name", "a_type");
NsdServiceInfo reply2 = new NsdServiceInfo("another_name", "a_type");
NsdServiceInfo reply3 = new NsdServiceInfo("a_third_name", "a_type");
@@ -305,7 +328,7 @@
int key2 = getRequestKey(req ->
verify(mServiceConn, times(2)).discoverServices(req.capture(), any()));
- mCallback.onDiscoverServicesStarted(key2, reply1);
+ mCallback.onDiscoverServicesStarted(key2, request1);
verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStarted("a_type");
@@ -345,7 +368,7 @@
int key3 = getRequestKey(req ->
verify(mServiceConn, times(3)).discoverServices(req.capture(), any()));
- mCallback.onDiscoverServicesStarted(key3, reply1);
+ mCallback.onDiscoverServicesStarted(key3, request1);
verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStarted("a_type");
// Client unregisters immediately, it fails
@@ -369,6 +392,9 @@
NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class);
NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
+ NsdManager.RegistrationListener listener4 = mock(NsdManager.RegistrationListener.class);
+ NsdManager.RegistrationListener listener5 = mock(NsdManager.RegistrationListener.class);
+ NsdManager.RegistrationListener listener6 = mock(NsdManager.RegistrationListener.class);
NsdServiceInfo invalidService = new NsdServiceInfo(null, null);
NsdServiceInfo validService = new NsdServiceInfo("a_name", "_a_type._tcp");
@@ -378,6 +404,7 @@
"_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);
@@ -385,6 +412,33 @@
otherSubtypeUpdate.setPort(2222);
dotSyntaxSubtypeUpdate.setPort(2222);
+ NsdServiceInfo invalidMissingHostnameWithAddresses = new NsdServiceInfo(null, null);
+ invalidMissingHostnameWithAddresses.setHostAddresses(
+ List.of(
+ InetAddress.parseNumericAddress("192.168.82.14"),
+ InetAddress.parseNumericAddress("2001::1")));
+
+ NsdServiceInfo validCustomHostWithAddresses = new NsdServiceInfo(null, null);
+ validCustomHostWithAddresses.setHostname("a_host");
+ validCustomHostWithAddresses.setHostAddresses(
+ List.of(
+ InetAddress.parseNumericAddress("192.168.82.14"),
+ InetAddress.parseNumericAddress("2001::1")));
+
+ NsdServiceInfo validServiceWithCustomHostAndAddresses =
+ new NsdServiceInfo("a_name", "_a_type._tcp");
+ validServiceWithCustomHostAndAddresses.setPort(2222);
+ validServiceWithCustomHostAndAddresses.setHostname("a_host");
+ validServiceWithCustomHostAndAddresses.setHostAddresses(
+ List.of(
+ InetAddress.parseNumericAddress("192.168.82.14"),
+ InetAddress.parseNumericAddress("2001::1")));
+
+ NsdServiceInfo validServiceWithCustomHostNoAddresses =
+ new NsdServiceInfo("a_name", "_a_type._tcp");
+ validServiceWithCustomHostNoAddresses.setPort(2222);
+ validServiceWithCustomHostNoAddresses.setHostname("a_host");
+
// Service registration
// - invalid arguments
mustFail(() -> { manager.unregisterService(null); });
@@ -393,6 +447,8 @@
mustFail(() -> { manager.registerService(invalidService, PROTOCOL, listener1); });
mustFail(() -> { manager.registerService(validService, -1, listener1); });
mustFail(() -> { manager.registerService(validService, PROTOCOL, null); });
+ mustFail(() -> {
+ manager.registerService(invalidMissingHostnameWithAddresses, PROTOCOL, listener1); });
manager.registerService(validService, PROTOCOL, listener1);
// - update without subtype is not allowed
mustFail(() -> { manager.registerService(validServiceDuplicate, PROTOCOL, listener1); });
@@ -414,6 +470,15 @@
// TODO: make listener immediately reusable
//mustFail(() -> { manager.unregisterService(listener1); });
//manager.registerService(validService, PROTOCOL, listener1);
+ // - registering a custom host without a service is valid
+ manager.registerService(validCustomHostWithAddresses, PROTOCOL, listener4);
+ manager.unregisterService(listener4);
+ // - registering a service with a custom host is valid
+ manager.registerService(validServiceWithCustomHostAndAddresses, PROTOCOL, listener5);
+ manager.unregisterService(listener5);
+ // - registering a service with a custom host with no addresses is valid
+ manager.registerService(validServiceWithCustomHostNoAddresses, PROTOCOL, listener6);
+ manager.unregisterService(listener6);
// Discover service
// - invalid arguments
@@ -455,4 +520,12 @@
verifier.accept(captor);
return captor.getValue();
}
+
+ AdvertisingRequest getAdvertisingRequest(
+ ThrowingConsumer<ArgumentCaptor<AdvertisingRequest>> verifier) throws Exception {
+ final ArgumentCaptor<AdvertisingRequest> captor =
+ ArgumentCaptor.forClass(AdvertisingRequest.class);
+ verifier.accept(captor);
+ return captor.getValue();
+ }
}
diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.kt b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.kt
new file mode 100644
index 0000000..8f86f06
--- /dev/null
+++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.nsd
+
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit tests for {@link NsdServiceInfo}. */
+@SmallTest
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class NsdServiceInfoTest {
+ @Test
+ fun testToString_txtRecord() {
+ val info = NsdServiceInfo().apply {
+ this.setAttribute("abc", byteArrayOf(0xff.toByte(), 0xfe.toByte()))
+ this.setAttribute("def", null as String?)
+ this.setAttribute("ghi", "猫")
+ this.setAttribute("jkl", byteArrayOf(0, 0x21))
+ this.setAttribute("mno", "Hey Tom! It's you?.~{}")
+ }
+
+ val infoStr = info.toString()
+
+ assertTrue(
+ infoStr.contains("txtRecord: " +
+ "{abc=0xFFFE, def=(null), ghi=0xE78CAB, jkl=0x0021, mno=Hey Tom! It's you?.~{}}"),
+ infoStr)
+ }
+}
diff --git a/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
index cb3a315..470274d 100644
--- a/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
+++ b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
@@ -95,11 +95,11 @@
// Check resource with invalid transport type.
assertRunWithException(arrayOf("-1,3"))
- assertRunWithException(arrayOf("10,3"))
+ assertRunWithException(arrayOf("11,3"))
// Check valid customization generates expected array.
val validRes = arrayOf("0,3", "1,0", "4,4")
- val expectedValidRes = intArrayOf(3, 0, 0, 0, 4, 0, 0, 0, 0, 0)
+ val expectedValidRes = intArrayOf(3, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0)
val mockContext = getMockedContextWithStringArrayRes(
R.array.config_networkSupportedKeepaliveCount,
diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
deleted file mode 100644
index acae7d2..0000000
--- a/tests/unit/java/com/android/internal/net/VpnProfileTest.java
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.net;
-
-import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V4;
-
-import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
-import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
-import static com.android.testutils.ParcelUtils.assertParcelSane;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import android.net.IpSecAlgorithm;
-import android.net.ipsec.ike.IkeTunnelConnectionParams;
-import android.os.Build;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/** Unit tests for {@link VpnProfile}. */
-@SmallTest
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-public class VpnProfileTest {
- private static final String DUMMY_PROFILE_KEY = "Test";
-
- private static final int ENCODED_INDEX_AUTH_PARAMS_INLINE = 23;
- private static final int ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS = 24;
- private static final int ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE = 25;
- private static final int ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION = 26;
- private static final int ENCODED_INDEX_IKE_TUN_CONN_PARAMS = 27;
- private static final int ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED = 28;
- private static final int ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED = 29;
-
- @Test
- public void testDefaults() throws Exception {
- final VpnProfile p = new VpnProfile(DUMMY_PROFILE_KEY);
-
- assertEquals(DUMMY_PROFILE_KEY, p.key);
- assertEquals("", p.name);
- assertEquals(VpnProfile.TYPE_PPTP, p.type);
- assertEquals("", p.server);
- assertEquals("", p.username);
- assertEquals("", p.password);
- assertEquals("", p.dnsServers);
- assertEquals("", p.searchDomains);
- assertEquals("", p.routes);
- assertTrue(p.mppe);
- assertEquals("", p.l2tpSecret);
- assertEquals("", p.ipsecIdentifier);
- assertEquals("", p.ipsecSecret);
- assertEquals("", p.ipsecUserCert);
- assertEquals("", p.ipsecCaCert);
- assertEquals("", p.ipsecServerCert);
- assertEquals(null, p.proxy);
- assertTrue(p.getAllowedAlgorithms() != null && p.getAllowedAlgorithms().isEmpty());
- assertFalse(p.isBypassable);
- assertFalse(p.isMetered);
- assertEquals(1360, p.maxMtu);
- assertFalse(p.areAuthParamsInline);
- assertFalse(p.isRestrictedToTestNetworks);
- assertFalse(p.excludeLocalRoutes);
- assertFalse(p.requiresInternetValidation);
- assertFalse(p.automaticNattKeepaliveTimerEnabled);
- assertFalse(p.automaticIpVersionSelectionEnabled);
- }
-
- private VpnProfile getSampleIkev2Profile(String key) {
- final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
- false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
- null /* ikeTunConnParams */, true /* mAutomaticNattKeepaliveTimerEnabled */,
- true /* automaticIpVersionSelectionEnabled */);
-
- p.name = "foo";
- p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;
- p.server = "bar";
- p.username = "baz";
- p.password = "qux";
- p.dnsServers = "8.8.8.8";
- p.searchDomains = "";
- p.routes = "0.0.0.0/0";
- p.mppe = false;
- p.l2tpSecret = "";
- p.ipsecIdentifier = "quux";
- p.ipsecSecret = "quuz";
- p.ipsecUserCert = "corge";
- p.ipsecCaCert = "grault";
- p.ipsecServerCert = "garply";
- p.proxy = null;
- p.setAllowedAlgorithms(
- Arrays.asList(
- IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
- IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305,
- IpSecAlgorithm.AUTH_HMAC_SHA512,
- IpSecAlgorithm.CRYPT_AES_CBC));
- p.isBypassable = true;
- p.isMetered = true;
- p.maxMtu = 1350;
- p.areAuthParamsInline = true;
-
- // Not saved, but also not compared.
- p.saveLogin = true;
-
- return p;
- }
-
- private VpnProfile getSampleIkev2ProfileWithIkeTunConnParams(String key) {
- final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
- false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
- new IkeTunnelConnectionParams(IKE_PARAMS_V4, CHILD_PARAMS),
- true /* mAutomaticNattKeepaliveTimerEnabled */,
- true /* automaticIpVersionSelectionEnabled */);
-
- p.name = "foo";
- p.server = "bar";
- p.dnsServers = "8.8.8.8";
- p.searchDomains = "";
- p.routes = "0.0.0.0/0";
- p.mppe = false;
- p.proxy = null;
- p.setAllowedAlgorithms(
- Arrays.asList(
- IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
- IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305,
- IpSecAlgorithm.AUTH_HMAC_SHA512,
- IpSecAlgorithm.CRYPT_AES_CBC));
- p.isBypassable = true;
- p.isMetered = true;
- p.maxMtu = 1350;
- p.areAuthParamsInline = true;
-
- // Not saved, but also not compared.
- p.saveLogin = true;
-
- return p;
- }
-
- @Test
- public void testEquals() {
- assertEquals(
- getSampleIkev2Profile(DUMMY_PROFILE_KEY), getSampleIkev2Profile(DUMMY_PROFILE_KEY));
-
- final VpnProfile modified = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
- modified.maxMtu--;
- assertNotEquals(getSampleIkev2Profile(DUMMY_PROFILE_KEY), modified);
- }
-
- @Test
- public void testParcelUnparcel() {
- if (isAtLeastU()) {
- // automaticNattKeepaliveTimerEnabled, automaticIpVersionSelectionEnabled added in U.
- assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 28);
- assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 28);
- } else if (isAtLeastT()) {
- // excludeLocalRoutes, requiresPlatformValidation were added in T.
- assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 26);
- assertParcelSane(getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY), 26);
- } else {
- assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 23);
- }
- }
-
- @Test
- public void testEncodeDecodeWithIkeTunConnParams() {
- final VpnProfile profile = getSampleIkev2ProfileWithIkeTunConnParams(DUMMY_PROFILE_KEY);
- final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
- assertEquals(profile, decoded);
- }
-
- @Test
- public void testEncodeDecode() {
- final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
- final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
- assertEquals(profile, decoded);
- }
-
- @Test
- public void testEncodeDecodeTooManyValues() {
- final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
- final byte[] tooManyValues =
- (new String(profile.encode()) + VpnProfile.VALUE_DELIMITER + "invalid").getBytes();
-
- assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooManyValues));
- }
-
- private String getEncodedDecodedIkev2ProfileMissingValues(int... missingIndices) {
- // Sort to ensure when we remove, we can do it from greatest first.
- Arrays.sort(missingIndices);
-
- final String encoded = new String(getSampleIkev2Profile(DUMMY_PROFILE_KEY).encode());
- final List<String> parts =
- new ArrayList<>(Arrays.asList(encoded.split(VpnProfile.VALUE_DELIMITER)));
-
- // Remove from back first to ensure indexing is consistent.
- for (int i = missingIndices.length - 1; i >= 0; i--) {
- parts.remove(missingIndices[i]);
- }
-
- return String.join(VpnProfile.VALUE_DELIMITER, parts.toArray(new String[0]));
- }
-
- @Test
- public void testEncodeDecodeInvalidNumberOfValues() {
- final String tooFewValues =
- getEncodedDecodedIkev2ProfileMissingValues(
- ENCODED_INDEX_AUTH_PARAMS_INLINE,
- ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS,
- ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE,
- ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION,
- ENCODED_INDEX_IKE_TUN_CONN_PARAMS,
- ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED,
- ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED
- /* missingIndices */);
-
- assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()));
- }
-
- private String getEncodedDecodedIkev2ProfileWithtooFewValues() {
- return getEncodedDecodedIkev2ProfileMissingValues(
- ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS,
- ENCODED_INDEX_EXCLUDE_LOCAL_ROUTE,
- ENCODED_INDEX_REQUIRE_PLATFORM_VALIDATION,
- ENCODED_INDEX_IKE_TUN_CONN_PARAMS,
- ENCODED_INDEX_AUTOMATIC_NATT_KEEPALIVE_TIMER_ENABLED,
- ENCODED_INDEX_AUTOMATIC_IP_VERSION_SELECTION_ENABLED /* missingIndices */);
- }
-
- @Test
- public void testEncodeDecodeMissingIsRestrictedToTestNetworks() {
- final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
-
- // Verify decoding without isRestrictedToTestNetworks defaults to false
- final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
- assertFalse(decoded.isRestrictedToTestNetworks);
- }
-
- @Test
- public void testEncodeDecodeMissingExcludeLocalRoutes() {
- final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
-
- // Verify decoding without excludeLocalRoutes defaults to false
- final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
- assertFalse(decoded.excludeLocalRoutes);
- }
-
- @Test
- public void testEncodeDecodeMissingRequiresValidation() {
- final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
-
- // Verify decoding without requiresValidation defaults to false
- final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
- assertFalse(decoded.requiresInternetValidation);
- }
-
- @Test
- public void testEncodeDecodeMissingAutomaticNattKeepaliveTimerEnabled() {
- final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
-
- // Verify decoding without automaticNattKeepaliveTimerEnabled defaults to false
- final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
- assertFalse(decoded.automaticNattKeepaliveTimerEnabled);
- }
-
- @Test
- public void testEncodeDecodeMissingAutomaticIpVersionSelectionEnabled() {
- final String tooFewValues = getEncodedDecodedIkev2ProfileWithtooFewValues();
-
- // Verify decoding without automaticIpVersionSelectionEnabled defaults to false
- final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
- assertFalse(decoded.automaticIpVersionSelectionEnabled);
- }
-
- @Test
- public void testEncodeDecodeLoginsNotSaved() {
- final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
- profile.saveLogin = false;
-
- final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
- assertNotEquals(profile, decoded);
-
- // Add the username/password back, everything else must be equal.
- decoded.username = profile.username;
- decoded.password = profile.password;
- assertEquals(profile, decoded);
- }
-
- @Test
- public void testClone() {
- final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
- final VpnProfile clone = profile.clone();
- assertEquals(profile, clone);
- assertNotSame(profile, clone);
- }
-}
diff --git a/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
index 3043d50..8a9286f 100644
--- a/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
+++ b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
@@ -1,5 +1,6 @@
package com.android.metrics
+import android.net.ConnectivityThread
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.CONNECTIVITY_MANAGED_CAPABILITIES
import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
@@ -15,12 +16,20 @@
import android.net.NetworkCapabilities.NET_ENTERPRISE_ID_3
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
import android.net.NetworkScore.POLICY_EXITING
import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY
import android.os.Build
import android.os.Handler
+import android.os.Process
+import android.os.Process.SYSTEM_UID
import android.stats.connectivity.MeteredState
+import android.stats.connectivity.RequestType
+import android.stats.connectivity.RequestType.RT_APP
+import android.stats.connectivity.RequestType.RT_SYSTEM
+import android.stats.connectivity.RequestType.RT_SYSTEM_ON_BEHALF_OF_APP
import android.stats.connectivity.ValidatedState
import androidx.test.filters.SmallTest
import com.android.net.module.util.BitUtils
@@ -30,11 +39,13 @@
import com.android.server.connectivity.FullScore.POLICY_IS_UNMETERED
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
-import org.junit.Test
-import org.junit.runner.RunWith
+import com.android.testutils.TestableNetworkCallback
import java.util.concurrent.CompletableFuture
import kotlin.test.assertEquals
import kotlin.test.fail
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
private fun <T> Handler.onHandler(f: () -> T): T {
val future = CompletableFuture<T>()
@@ -79,14 +90,17 @@
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
class ConnectivitySampleMetricsTest : CSTest() {
@Test
- fun testSampleConnectivityState() {
+ fun testSampleConnectivityState_Network() {
val wifi1Caps = NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_WIFI)
.addCapability(NET_CAPABILITY_NOT_METERED)
.addCapability(NET_CAPABILITY_NOT_SUSPENDED)
.addCapability(NET_CAPABILITY_NOT_ROAMING)
.build()
- val wifi1Score = NetworkScore.Builder().setExiting(true).build()
+ val wifi1Score = NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+ .setExiting(true)
+ .build()
val agentWifi1 = Agent(nc = wifi1Caps, score = FromS(wifi1Score)).also { it.connect() }
val wifi2Caps = NetworkCapabilities.Builder()
@@ -96,7 +110,10 @@
.addCapability(NET_CAPABILITY_NOT_ROAMING)
.addEnterpriseId(NET_ENTERPRISE_ID_3)
.build()
- val wifi2Score = NetworkScore.Builder().setTransportPrimary(true).build()
+ val wifi2Score = NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+ .setTransportPrimary(true)
+ .build()
val agentWifi2 = Agent(nc = wifi2Caps, score = FromS(wifi2Score)).also { it.connect() }
val cellCaps = NetworkCapabilities.Builder()
@@ -107,7 +124,9 @@
.addCapability(NET_CAPABILITY_NOT_ROAMING)
.addEnterpriseId(NET_ENTERPRISE_ID_1)
.build()
- val cellScore = NetworkScore.Builder().build()
+ val cellScore = NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+ .build()
val agentCell = Agent(nc = cellCaps, score = FromS(cellScore)).also { it.connect() }
val stats = csHandler.onHandler { service.sampleConnectivityState() }
@@ -170,4 +189,61 @@
"expected ${expectedWifi2Policies.toPolicyString()}, " +
"found ${foundWifi2.scorePolicies.toPolicyString()}")
}
+
+ private fun fileNetworkRequest(requestType: RequestType, requestCount: Int, uid: Int? = null) {
+ if (uid != null) {
+ deps.setCallingUid(uid)
+ }
+ try {
+ repeat(requestCount) {
+ when (requestType) {
+ RT_APP, RT_SYSTEM -> cm.requestNetwork(
+ NetworkRequest.Builder().build(),
+ TestableNetworkCallback()
+ )
+
+ RT_SYSTEM_ON_BEHALF_OF_APP -> cm.registerDefaultNetworkCallbackForUid(
+ Process.myUid(),
+ TestableNetworkCallback(),
+ Handler(ConnectivityThread.getInstanceLooper()))
+
+ else -> fail("invalid requestType: " + requestType)
+ }
+ }
+ } finally {
+ deps.unmockCallingUid()
+ }
+ }
+
+
+ @Test
+ fun testSampleConnectivityState_NetworkRequest() {
+ val requestCount = 5
+ fileNetworkRequest(RT_APP, requestCount);
+ fileNetworkRequest(RT_SYSTEM, requestCount, SYSTEM_UID);
+ fileNetworkRequest(RT_SYSTEM_ON_BEHALF_OF_APP, requestCount, SYSTEM_UID);
+
+ val stats = csHandler.onHandler { service.sampleConnectivityState() }
+
+ assertEquals(3, stats.networkRequestCount.requestCountForTypeList.size)
+ val appRequest = stats.networkRequestCount.requestCountForTypeList.find {
+ it.requestType == RT_APP
+ } ?: fail("Can't find RT_APP request")
+ val systemRequest = stats.networkRequestCount.requestCountForTypeList.find {
+ it.requestType == RT_SYSTEM
+ } ?: fail("Can't find RT_SYSTEM request")
+ val systemOnBehalfOfAppRequest = stats.networkRequestCount.requestCountForTypeList.find {
+ it.requestType == RT_SYSTEM_ON_BEHALF_OF_APP
+ } ?: fail("Can't find RT_SYSTEM_ON_BEHALF_OF_APP request")
+
+ // Verify request count is equal or larger than the number of request this test filed
+ // since ConnectivityService internally files network requests
+ assertTrue("Unexpected RT_APP count, expected >= $requestCount, " +
+ "found ${appRequest.requestCount}", appRequest.requestCount >= requestCount)
+ assertTrue("Unexpected RT_SYSTEM count, expected >= $requestCount, " +
+ "found ${systemRequest.requestCount}", systemRequest.requestCount >= requestCount)
+ assertTrue("Unexpected RT_SYSTEM_ON_BEHALF_OF_APP count, expected >= $requestCount, " +
+ "found ${systemOnBehalfOfAppRequest.requestCount}",
+ systemOnBehalfOfAppRequest.requestCount >= requestCount)
+ }
}
diff --git a/tests/unit/java/com/android/server/BpfLoaderRcUtilsTest.kt b/tests/unit/java/com/android/server/BpfLoaderRcUtilsTest.kt
deleted file mode 100644
index 2cf6b17..0000000
--- a/tests/unit/java/com/android/server/BpfLoaderRcUtilsTest.kt
+++ /dev/null
@@ -1,72 +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
-
-import android.os.Build
-import androidx.test.filters.SmallTest
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
-import com.android.testutils.DevSdkIgnoreRunner
-import kotlin.test.assertEquals
-import kotlin.test.assertTrue
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(DevSdkIgnoreRunner::class)
-@SmallTest
-@IgnoreUpTo(Build.VERSION_CODES.S)
-class BpfLoaderRcUtilsTest {
- @Test
- fun testLoadExistingBpfRcFile() {
-
- val inputString = """
- service a
- # test comment
- service bpfloader /system/bin/bpfloader
- capabilities CHOWN SYS_ADMIN NET_ADMIN
- group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
- user root
- rlimit memlock 1073741824 1073741824
- oneshot
- # comment 漢字
- reboot_on_failure reboot,bpfloader-failed
- updatable
-
- #test comment
- on b
- oneshot
- # test comment
- """.trimIndent()
- val expectedResult = listOf(
- "service bpfloader /system/bin/bpfloader",
- "capabilities CHOWN SYS_ADMIN NET_ADMIN",
- "group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system",
- "user root",
- "rlimit memlock 1073741824 1073741824",
- "oneshot",
- "reboot_on_failure reboot,bpfloader-failed",
- "updatable"
- )
-
- assertEquals(expectedResult,
- BpfLoaderRcUtils.loadExistingBpfRcFile(inputString.byteInputStream()))
- }
-
- @Test
- fun testCheckBpfRcFile() {
- assertTrue(BpfLoaderRcUtils.checkBpfLoaderRc())
- }
-}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 8f5fd7c..7822fe0 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -157,11 +157,11 @@
import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH;
import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
+import static com.android.server.ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK;
import static com.android.server.ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION;
import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
import static com.android.server.ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS;
import static com.android.server.ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION;
-import static com.android.server.ConnectivityService.LOG_BPF_RC;
import static com.android.server.ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID;
import static com.android.server.ConnectivityService.PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED;
import static com.android.server.ConnectivityService.PREFERENCE_ORDER_OEM;
@@ -172,6 +172,8 @@
import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackRegister;
import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister;
+import static com.android.server.connectivity.ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN;
+import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
import static com.android.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.ConcurrentUtils.await;
import static com.android.testutils.ConcurrentUtils.durationOf;
@@ -367,7 +369,6 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
-import android.security.Credentials;
import android.system.Os;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
@@ -388,10 +389,10 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IBatteryStats;
import com.android.internal.net.VpnConfig;
-import com.android.internal.net.VpnProfile;
import com.android.internal.util.WakeupMessage;
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.ArrayTrackRecord;
import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
import com.android.net.module.util.CollectionUtils;
@@ -420,9 +421,9 @@
import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
import com.android.server.connectivity.ProxyTracker;
import com.android.server.connectivity.QosCallbackTracker;
+import com.android.server.connectivity.SatelliteAccessController;
import com.android.server.connectivity.TcpKeepaliveController;
import com.android.server.connectivity.UidRangeUtils;
-import com.android.server.connectivity.VpnProfileStore;
import com.android.server.net.NetworkPinner;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -462,7 +463,6 @@
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
-import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -485,6 +485,7 @@
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -502,6 +503,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";
@@ -522,7 +524,7 @@
// between a LOST callback that arrives immediately and a LOST callback that arrives after
// the linger/nascent timeout. For this, our assertions should run fast enough to leave
// less than (mService.mLingerDelayMs - TEST_CALLBACK_TIMEOUT_MS) between the time callbacks are
- // supposedly fired, and the time we call expectCallback.
+ // supposedly fired, and the time we call expectCapChanged.
private static final int TEST_CALLBACK_TIMEOUT_MS = 250;
// Chosen to be less than TEST_CALLBACK_TIMEOUT_MS. This ensures that requests have time to
// complete before callbacks are verified.
@@ -561,6 +563,7 @@
private static final int TEST_PACKAGE_UID2 = 321;
private static final int TEST_PACKAGE_UID3 = 456;
private static final int NETWORK_ACTIVITY_NO_UID = -1;
+ private static final int TEST_SUBSCRIPTION_ID = 1;
private static final int PACKET_WAKEUP_MARK_MASK = 0x80000000;
@@ -589,6 +592,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;
@@ -625,7 +629,6 @@
@Mock TelephonyManager mTelephonyManager;
@Mock EthernetManager mEthernetManager;
@Mock NetworkPolicyManager mNetworkPolicyManager;
- @Mock VpnProfileStore mVpnProfileStore;
@Mock SystemConfigManager mSystemConfigManager;
@Mock DevicePolicyManager mDevicePolicyManager;
@Mock Resources mResources;
@@ -639,6 +642,7 @@
@Mock DestroySocketsWrapper mDestroySocketsWrapper;
@Mock SubscriptionManager mSubscriptionManager;
@Mock KeepaliveTracker.Dependencies mMockKeepaliveTrackerDependencies;
+ @Mock SatelliteAccessController mSatelliteAccessController;
// BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the
// underlying binder calls.
@@ -1092,6 +1096,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.
@@ -1659,23 +1664,11 @@
waitForIdle();
}
- public void startLegacyVpnPrivileged(VpnProfile profile) {
- switch (profile.type) {
- case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
- case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
- case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
- case VpnProfile.TYPE_IKEV2_FROM_IKE_TUN_CONN_PARAMS:
- startPlatformVpn();
- break;
- case VpnProfile.TYPE_L2TP_IPSEC_PSK:
- case VpnProfile.TYPE_L2TP_IPSEC_RSA:
- case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
- case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
- case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
- startLegacyVpn();
- break;
- default:
- fail("Unknown VPN profile type");
+ public void startLegacyVpnPrivileged(boolean isIkev2Vpn) {
+ if (isIkev2Vpn) {
+ startPlatformVpn();
+ } else {
+ startLegacyVpn();
}
}
@@ -1728,6 +1721,8 @@
private void mockUidNetworkingBlocked() {
doAnswer(i -> isUidBlocked(mBlockedReasons, i.getArgument(1))
).when(mNetworkPolicyManager).isUidNetworkingBlocked(anyInt(), anyBoolean());
+ doAnswer(i -> isUidBlocked(mBlockedReasons, i.getArgument(1))
+ ).when(mBpfNetMaps).isUidNetworkingBlocked(anyInt(), anyBoolean());
}
private boolean isUidBlocked(int blockedReasons, boolean meteredNetwork) {
@@ -2047,14 +2042,28 @@
};
}
+ private BiConsumer<Integer, Integer> mCarrierPrivilegesLostListener;
+
@Override
public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
@NonNull final Context context,
- @NonNull final TelephonyManager tm) {
+ @NonNull final TelephonyManager tm,
+ final boolean requestRestrictedWifiEnabled,
+ BiConsumer<Integer, Integer> listener,
+ @NonNull final Handler handler) {
+ mCarrierPrivilegesLostListener = listener;
return mDeps.isAtLeastT() ? mCarrierPrivilegeAuthenticator : null;
}
@Override
+ public SatelliteAccessController makeSatelliteAccessController(
+ @NonNull final Context context,
+ Consumer<Set<Integer>> updateSatelliteNetworkFallbackUidCallback,
+ @NonNull final Handler connectivityServiceInternalHandler) {
+ return mSatelliteAccessController;
+ }
+
+ @Override
public boolean intentFilterEquals(final PendingIntent a, final PendingIntent b) {
return runAsShell(GET_INTENT_SENDER_INTENT, () -> a.intentFilterEquals(b));
}
@@ -2144,6 +2153,8 @@
case ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER:
case ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK:
return true;
+ case ConnectivityFlags.REQUEST_RESTRICTED_WIFI:
+ return true;
case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
return true;
case DELAY_DESTROY_FROZEN_SOCKETS_VERSION:
@@ -2158,7 +2169,11 @@
switch (name) {
case ALLOW_SYSUI_CONNECTIVITY_REPORTS:
return true;
- case LOG_BPF_RC:
+ case ALLOW_SATALLITE_NETWORK_FALLBACK:
+ return true;
+ case INGRESS_TO_VPN_ADDRESS_FILTERING:
+ return true;
+ case BACKGROUND_FIREWALL_CHAIN:
return true;
default:
return super.isFeatureNotChickenedOut(context, name);
@@ -2404,6 +2419,11 @@
FakeSettingsProvider.clearSettingsProvider();
ConnectivityResources.setResourcesContextForTest(null);
+ for (TestNetworkAgentWrapper agent : mCreatedAgents) {
+ agent.destroy();
+ }
+ mCreatedAgents.clear();
+
mCsHandlerThread.quitSafely();
mCsHandlerThread.join();
mAlarmManagerThread.quitSafely();
@@ -10186,24 +10206,6 @@
doAsUid(Process.SYSTEM_UID, () -> mCm.unregisterNetworkCallback(perUidCb));
}
- private VpnProfile setupLockdownVpn(int profileType) {
- final String profileName = "testVpnProfile";
- final byte[] profileTag = profileName.getBytes(StandardCharsets.UTF_8);
- doReturn(profileTag).when(mVpnProfileStore).get(Credentials.LOCKDOWN_VPN);
-
- final VpnProfile profile = new VpnProfile(profileName);
- profile.name = "My VPN";
- profile.server = "192.0.2.1";
- profile.dnsServers = "8.8.8.8";
- profile.ipsecIdentifier = "My ipsecIdentifier";
- profile.ipsecSecret = "My PSK";
- profile.type = profileType;
- final byte[] encodedProfile = profile.encode();
- doReturn(encodedProfile).when(mVpnProfileStore).get(Credentials.VPN + profileName);
-
- return profile;
- }
-
private void establishLegacyLockdownVpn(Network underlying) throws Exception {
// The legacy lockdown VPN only supports userId 0, and must have an underlying network.
assertNotNull(underlying);
@@ -10215,7 +10217,7 @@
mMockVpn.connect(true);
}
- private void doTestLockdownVpn(VpnProfile profile, boolean expectSetVpnDefaultForUids)
+ private void doTestLockdownVpn(boolean isIkev2Vpn)
throws Exception {
mServiceContext.setPermission(
Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
@@ -10253,8 +10255,8 @@
b.expectBroadcast();
// Simulate LockdownVpnTracker attempting to start the VPN since it received the
// systemDefault callback.
- mMockVpn.startLegacyVpnPrivileged(profile);
- if (expectSetVpnDefaultForUids) {
+ mMockVpn.startLegacyVpnPrivileged(isIkev2Vpn);
+ if (isIkev2Vpn) {
// setVpnDefaultForUids() releases the original network request and creates a VPN
// request so LOST callback is received.
defaultCallback.expect(LOST, mCellAgent);
@@ -10278,7 +10280,7 @@
final NetworkCapabilities vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
b2.expectBroadcast();
b3.expectBroadcast();
- if (expectSetVpnDefaultForUids) {
+ if (isIkev2Vpn) {
// Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
// network satisfier which has TYPE_VPN.
assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
@@ -10324,14 +10326,15 @@
// callback with different network.
final ExpectedBroadcast b6 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
mMockVpn.stopVpnRunnerPrivileged();
- mMockVpn.startLegacyVpnPrivileged(profile);
+
+ mMockVpn.startLegacyVpnPrivileged(isIkev2Vpn);
// VPN network is disconnected (to restart)
callback.expect(LOST, mMockVpn);
defaultCallback.expect(LOST, mMockVpn);
// The network preference is cleared when VPN is disconnected so it receives callbacks for
// the system-wide default.
defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiAgent);
- if (expectSetVpnDefaultForUids) {
+ if (isIkev2Vpn) {
// setVpnDefaultForUids() releases the original network request and creates a VPN
// request so LOST callback is received.
defaultCallback.expect(LOST, mWiFiAgent);
@@ -10340,7 +10343,7 @@
b6.expectBroadcast();
// While the VPN is reconnecting on the new network, everything is blocked.
- if (expectSetVpnDefaultForUids) {
+ if (isIkev2Vpn) {
// Due to the VPN default request, getActiveNetworkInfo() gets the mNoServiceNetwork
// as the network satisfier.
assertNull(mCm.getActiveNetworkInfo());
@@ -10361,7 +10364,7 @@
systemDefaultCallback.assertNoCallback();
b7.expectBroadcast();
b8.expectBroadcast();
- if (expectSetVpnDefaultForUids) {
+ if (isIkev2Vpn) {
// Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
// network satisfier which has TYPE_VPN.
assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
@@ -10387,7 +10390,7 @@
defaultCallback.assertNoCallback();
systemDefaultCallback.assertNoCallback();
- if (expectSetVpnDefaultForUids) {
+ if (isIkev2Vpn) {
// Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
// network satisfier which has TYPE_VPN.
assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
@@ -10428,14 +10431,12 @@
@Test
public void testLockdownVpn_LegacyVpnRunner() throws Exception {
- final VpnProfile profile = setupLockdownVpn(VpnProfile.TYPE_IPSEC_XAUTH_PSK);
- doTestLockdownVpn(profile, false /* expectSetVpnDefaultForUids */);
+ doTestLockdownVpn(false /* isIkev2Vpn */);
}
@Test
public void testLockdownVpn_Ikev2VpnRunner() throws Exception {
- final VpnProfile profile = setupLockdownVpn(VpnProfile.TYPE_IKEV2_IPSEC_PSK);
- doTestLockdownVpn(profile, true /* expectSetVpnDefaultForUids */);
+ doTestLockdownVpn(true /* isIkev2Vpn */);
}
@Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -10491,7 +10492,10 @@
doTestSetUidFirewallRule(FIREWALL_CHAIN_POWERSAVE, FIREWALL_RULE_DENY);
doTestSetUidFirewallRule(FIREWALL_CHAIN_RESTRICTED, FIREWALL_RULE_DENY);
doTestSetUidFirewallRule(FIREWALL_CHAIN_LOW_POWER_STANDBY, FIREWALL_RULE_DENY);
- doTestSetUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, FIREWALL_RULE_DENY);
+ if (SdkLevel.isAtLeastV()) {
+ // FIREWALL_CHAIN_BACKGROUND is only available on V+.
+ doTestSetUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, FIREWALL_RULE_DENY);
+ }
doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_1, FIREWALL_RULE_ALLOW);
doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_2, FIREWALL_RULE_ALLOW);
doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_3, FIREWALL_RULE_ALLOW);
@@ -10499,16 +10503,19 @@
@Test @IgnoreUpTo(SC_V2)
public void testSetFirewallChainEnabled() throws Exception {
- final List<Integer> firewallChains = Arrays.asList(
+ final List<Integer> firewallChains = new ArrayList<>(Arrays.asList(
FIREWALL_CHAIN_DOZABLE,
FIREWALL_CHAIN_STANDBY,
FIREWALL_CHAIN_POWERSAVE,
FIREWALL_CHAIN_RESTRICTED,
FIREWALL_CHAIN_LOW_POWER_STANDBY,
- FIREWALL_CHAIN_BACKGROUND,
FIREWALL_CHAIN_OEM_DENY_1,
FIREWALL_CHAIN_OEM_DENY_2,
- FIREWALL_CHAIN_OEM_DENY_3);
+ FIREWALL_CHAIN_OEM_DENY_3));
+ if (SdkLevel.isAtLeastV()) {
+ // FIREWALL_CHAIN_BACKGROUND is only available on V+.
+ firewallChains.add(FIREWALL_CHAIN_BACKGROUND);
+ }
for (final int chain: firewallChains) {
mCm.setFirewallChainEnabled(chain, true /* enabled */);
verify(mBpfNetMaps).setChildChain(chain, true /* enable */);
@@ -10555,7 +10562,10 @@
doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_POWERSAVE, allowlist);
doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_RESTRICTED, allowlist);
doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_LOW_POWER_STANDBY, allowlist);
- doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_BACKGROUND, allowlist);
+ if (SdkLevel.isAtLeastV()) {
+ // FIREWALL_CHAIN_BACKGROUND is only available on V+.
+ doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_BACKGROUND, allowlist);
+ }
doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_STANDBY, denylist);
doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_OEM_DENY_1, denylist);
@@ -10577,7 +10587,10 @@
doTestReplaceFirewallChain(FIREWALL_CHAIN_POWERSAVE);
doTestReplaceFirewallChain(FIREWALL_CHAIN_RESTRICTED);
doTestReplaceFirewallChain(FIREWALL_CHAIN_LOW_POWER_STANDBY);
- doTestReplaceFirewallChain(FIREWALL_CHAIN_BACKGROUND);
+ if (SdkLevel.isAtLeastV()) {
+ // FIREWALL_CHAIN_BACKGROUND is only available on V+.
+ doTestReplaceFirewallChain(FIREWALL_CHAIN_BACKGROUND);
+ }
doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_1);
doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_2);
doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_3);
@@ -11460,7 +11473,7 @@
doTestInterfaceClassActivityChanged(TRANSPORT_CELLULAR);
}
- private void doTestOnNetworkActive_NewNetworkConnects(int transportType, boolean expectCallback)
+ private void doTestOnNetworkActive_NewNetworkConnects(int transportType, boolean expectCapChanged)
throws Exception {
final ConditionVariable onNetworkActiveCv = new ConditionVariable();
final ConnectivityManager.OnNetworkActiveListener listener = onNetworkActiveCv::open;
@@ -11472,7 +11485,7 @@
testAndCleanup(() -> {
mCm.addDefaultNetworkActiveListener(listener);
agent.connect(true);
- if (expectCallback) {
+ if (expectCapChanged) {
assertTrue(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
} else {
assertFalse(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
@@ -11487,7 +11500,7 @@
@Test
public void testOnNetworkActive_NewCellConnects_CallbackCalled() throws Exception {
- doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_CELLULAR, true /* expectCallback */);
+ doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_CELLULAR, true /* expectCapChanged */);
}
@Test
@@ -11496,8 +11509,8 @@
// networks that tracker adds the idle timer to. And the tracker does not set the idle timer
// for the ethernet network.
// So onNetworkActive is not called when the ethernet becomes the default network
- final boolean expectCallback = mDeps.isAtLeastV();
- doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, expectCallback);
+ final boolean expectCapChanged = mDeps.isAtLeastV();
+ doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, expectCapChanged);
}
@Test
@@ -12921,7 +12934,7 @@
mServiceContext.setPermission(NETWORK_STACK, PERMISSION_GRANTED);
assertTrue(
"NetworkStack permission not applied",
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid(), Process.myUid(), naiWithoutUid,
mContext.getOpPackageName()));
}
@@ -12933,7 +12946,7 @@
mServiceContext.setPermission(STATUS_BAR_SERVICE, PERMISSION_GRANTED);
assertTrue(
"SysUi permission (STATUS_BAR_SERVICE) not applied",
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid(), Process.myUid(), naiWithoutUid,
mContext.getOpPackageName()));
}
@@ -12950,7 +12963,7 @@
assertFalse(
"Mismatched uid/package name should not pass the location permission check",
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid() + 1, wrongUid, naiWithUid, mContext.getOpPackageName()));
}
@@ -12961,7 +12974,7 @@
assertEquals(
"Unexpected ConnDiags permission",
expectPermission,
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid(), Process.myUid(), info, mContext.getOpPackageName()));
}
@@ -13003,7 +13016,7 @@
waitForIdle();
assertTrue(
"Active VPN permission not applied",
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid(), Process.myUid(), naiWithoutUid,
mContext.getOpPackageName()));
@@ -13011,7 +13024,7 @@
waitForIdle();
assertFalse(
"VPN shouldn't receive callback on non-underlying network",
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid(), Process.myUid(), naiWithoutUid,
mContext.getOpPackageName()));
}
@@ -13028,7 +13041,7 @@
assertTrue(
"NetworkCapabilities administrator uid permission not applied",
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid(), Process.myUid(), naiWithUid, mContext.getOpPackageName()));
}
@@ -13046,7 +13059,7 @@
// Use wrong pid and uid
assertFalse(
"Permissions allowed when they shouldn't be granted",
- mService.checkConnectivityDiagnosticsPermissions(
+ mService.hasConnectivityDiagnosticsPermissions(
Process.myPid() + 1, Process.myUid() + 1, naiWithUid,
mContext.getOpPackageName()));
}
@@ -17314,21 +17327,7 @@
}
@Test
- public void testSubIdsClearedWithoutNetworkFactoryPermission() throws Exception {
- mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED);
- final NetworkCapabilities nc = new NetworkCapabilities();
- nc.setSubscriptionIds(Collections.singleton(Process.myUid()));
-
- final NetworkCapabilities result =
- mService.networkCapabilitiesRestrictedForCallerPermissions(
- nc, Process.myPid(), Process.myUid());
- assertTrue(result.getSubscriptionIds().isEmpty());
- }
-
- @Test
- public void testSubIdsExistWithNetworkFactoryPermission() throws Exception {
- mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
-
+ public void testSubIdsExist() throws Exception {
final Set<Integer> subIds = Collections.singleton(Process.myUid());
final NetworkCapabilities nc = new NetworkCapabilities();
nc.setSubscriptionIds(subIds);
@@ -17345,9 +17344,16 @@
.build();
}
+ private NetworkRequest getRestrictedRequestForWifiWithSubIds() {
+ return new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ .setSubscriptionIds(Collections.singleton(TEST_SUBSCRIPTION_ID))
+ .build();
+ }
+
@Test
- public void testNetworkRequestWithSubIdsWithNetworkFactoryPermission() throws Exception {
- mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
+ public void testNetworkRequestWithSubIds() throws Exception {
final PendingIntent pendingIntent = PendingIntent.getBroadcast(
mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE);
final NetworkCallback networkCallback1 = new NetworkCallback();
@@ -17363,18 +17369,183 @@
}
@Test
- public void testNetworkRequestWithSubIdsWithoutNetworkFactoryPermission() throws Exception {
- mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED);
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testCarrierConfigAppSendNetworkRequestForRestrictedWifi() throws Exception {
+ mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED);
+ doReturn(true).when(mCarrierPrivilegeAuthenticator)
+ .isCarrierServiceUidForNetworkCapabilities(anyInt(), any());
final PendingIntent pendingIntent = PendingIntent.getBroadcast(
mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE);
+ final NetworkCallback networkCallback1 = new NetworkCallback();
+ final NetworkCallback networkCallback2 = new NetworkCallback();
- final Class<SecurityException> expected = SecurityException.class;
- assertThrows(
- expected, () -> mCm.requestNetwork(getRequestWithSubIds(), new NetworkCallback()));
- assertThrows(expected, () -> mCm.requestNetwork(getRequestWithSubIds(), pendingIntent));
- assertThrows(
- expected,
- () -> mCm.registerNetworkCallback(getRequestWithSubIds(), new NetworkCallback()));
+ mCm.requestNetwork(
+ getRestrictedRequestForWifiWithSubIds(), networkCallback1);
+ mCm.requestNetwork(
+ getRestrictedRequestForWifiWithSubIds(), pendingIntent);
+ mCm.registerNetworkCallback(
+ getRestrictedRequestForWifiWithSubIds(), networkCallback2);
+
+ mCm.unregisterNetworkCallback(networkCallback1);
+ mCm.releaseNetworkRequest(pendingIntent);
+ mCm.unregisterNetworkCallback(networkCallback2);
+ }
+
+ private void doTestNetworkRequestWithCarrierPrivilegesLost(
+ boolean shouldGrantRestrictedNetworkPermission,
+ int lostPrivilegeUid,
+ int lostPrivilegeSubId,
+ boolean expectUnavailable,
+ boolean expectCapChanged) throws Exception {
+ if (shouldGrantRestrictedNetworkPermission) {
+ mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_GRANTED);
+ } else {
+ mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED);
+ }
+
+ NetworkCapabilities filter =
+ getRestrictedRequestForWifiWithSubIds().networkCapabilities;
+ final HandlerThread handlerThread = new HandlerThread("testRestrictedFactoryRequests");
+ handlerThread.start();
+
+ final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
+ mServiceContext, "testFactory", filter, mCsHandlerThread);
+ testFactory.register();
+ testFactory.assertRequestCountEquals(0);
+
+ doReturn(true).when(mCarrierPrivilegeAuthenticator)
+ .isCarrierServiceUidForNetworkCapabilities(eq(Process.myUid()), any());
+ final TestNetworkCallback networkCallback = new TestNetworkCallback();
+ final NetworkRequest networkrequest =
+ getRestrictedRequestForWifiWithSubIds();
+ mCm.requestNetwork(networkrequest, networkCallback);
+ testFactory.expectRequestAdd();
+ testFactory.assertRequestCountEquals(1);
+
+ NetworkCapabilities nc = new NetworkCapabilities.Builder(filter)
+ .setAllowedUids(Set.of(Process.myUid()))
+ .build();
+ mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(), nc);
+ mWiFiAgent.connect(false);
+ networkCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+ final NetworkAgentInfo nai = mService.getNetworkAgentInfoForNetwork(
+ mWiFiAgent.getNetwork());
+
+ doReturn(false).when(mCarrierPrivilegeAuthenticator)
+ .isCarrierServiceUidForNetworkCapabilities(eq(Process.myUid()), any());
+ doReturn(TEST_SUBSCRIPTION_ID).when(mCarrierPrivilegeAuthenticator)
+ .getSubIdFromNetworkCapabilities(any());
+
+ visibleOnHandlerThread(mCsHandlerThread.getThreadHandler(), () -> {
+ mDeps.mCarrierPrivilegesLostListener.accept(lostPrivilegeUid, lostPrivilegeSubId);
+ });
+ waitForIdle();
+
+ if (expectCapChanged) {
+ networkCallback.expect(NETWORK_CAPS_UPDATED);
+ }
+ if (expectUnavailable) {
+ networkCallback.expect(UNAVAILABLE);
+ }
+ if (!expectCapChanged && !expectUnavailable) {
+ networkCallback.assertNoCallback();
+ }
+
+ mWiFiAgent.disconnect();
+
+ if (expectUnavailable) {
+ testFactory.expectRequestRemove();
+ testFactory.assertRequestCountEquals(0);
+ } else {
+ testFactory.expectRequestAdd();
+ testFactory.assertRequestCountEquals(1);
+ }
+
+ handlerThread.quitSafely();
+ handlerThread.join();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testRestrictedRequestRemovedDueToCarrierPrivilegesLost() throws Exception {
+ doTestNetworkRequestWithCarrierPrivilegesLost(
+ false /* shouldGrantRestrictedNetworkPermission */,
+ Process.myUid(),
+ TEST_SUBSCRIPTION_ID,
+ true /* expectUnavailable */,
+ true /* expectCapChanged */);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testRequestNotRemoved_MismatchSubId() throws Exception {
+ doTestNetworkRequestWithCarrierPrivilegesLost(
+ false /* shouldGrantRestrictedNetworkPermission */,
+ Process.myUid(),
+ TEST_SUBSCRIPTION_ID + 1,
+ false /* expectUnavailable */,
+ false /* expectCapChanged */);
+ }
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testRequestNotRemoved_MismatchUid() throws Exception {
+ doTestNetworkRequestWithCarrierPrivilegesLost(
+ false /* shouldGrantRestrictedNetworkPermission */,
+ Process.myUid() + 1,
+ TEST_SUBSCRIPTION_ID,
+ false /* expectUnavailable */,
+ false /* expectCapChanged */);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testRequestNotRemoved_HasRestrictedNetworkPermission() throws Exception {
+ doTestNetworkRequestWithCarrierPrivilegesLost(
+ true /* shouldGrantRestrictedNetworkPermission */,
+ Process.myUid(),
+ TEST_SUBSCRIPTION_ID,
+ false /* expectUnavailable */,
+ true /* expectCapChanged */);
+ }
+
+ @Test
+ public void testAllowedUidsExistWithoutNetworkFactoryPermission() throws Exception {
+ // Make sure NETWORK_FACTORY permission is not granted.
+ mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED);
+ mServiceContext.setPermission(MANAGE_TEST_NETWORKS, PERMISSION_GRANTED);
+ final TestNetworkCallback cb = new TestNetworkCallback();
+ mCm.requestNetwork(new NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_TEST)
+ .addTransportType(TRANSPORT_CELLULAR)
+ .build(),
+ cb);
+
+ final ArraySet<Integer> uids = new ArraySet<>();
+ uids.add(200);
+ final NetworkCapabilities nc = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_TEST)
+ .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ .setAllowedUids(uids)
+ .setOwnerUid(Process.myUid())
+ .setAdministratorUids(new int[] {Process.myUid()})
+ .build();
+ final TestNetworkAgentWrapper agent = new TestNetworkAgentWrapper(TRANSPORT_TEST,
+ new LinkProperties(), nc);
+ agent.connect(true);
+ cb.expectAvailableThenValidatedCallbacks(agent);
+
+ uids.add(300);
+ uids.add(400);
+ nc.setAllowedUids(uids);
+ agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
+ if (mDeps.isAtLeastT()) {
+ // AllowedUids is not cleared even without the NETWORK_FACTORY permission
+ // because the caller is the owner of the network.
+ cb.expectCaps(agent, c -> c.getAllowedUids().equals(uids));
+ } else {
+ cb.assertNoCallback();
+ }
}
@Test
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 87e7967..d91e29c 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -34,6 +34,7 @@
import static android.net.connectivity.ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER;
import static android.net.nsd.NsdManager.FAILURE_BAD_PARAMETERS;
import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
+import static android.net.nsd.NsdManager.FAILURE_MAX_LIMIT;
import static android.net.nsd.NsdManager.FAILURE_OPERATION_NOT_RUNNING;
import static com.android.networkstack.apishim.api33.ConstantsShim.REGISTER_NSD_OFFLOAD_ENGINE;
@@ -82,6 +83,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.INsdManagerCallback;
import android.net.nsd.INsdServiceConnector;
import android.net.nsd.MDnsManager;
@@ -100,6 +102,7 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
+import android.os.Process;
import android.os.RemoteException;
import android.util.Pair;
@@ -109,6 +112,7 @@
import com.android.metrics.NetworkNsdReportedMetrics;
import com.android.server.NsdService.Dependencies;
import com.android.server.connectivity.mdns.MdnsAdvertiser;
+import com.android.server.connectivity.mdns.MdnsAdvertisingOptions;
import com.android.server.connectivity.mdns.MdnsDiscoveryManager;
import com.android.server.connectivity.mdns.MdnsInterfaceSocket;
import com.android.server.connectivity.mdns.MdnsSearchOptions;
@@ -131,10 +135,14 @@
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -234,7 +242,8 @@
doReturn(mSocketProvider).when(mDeps).makeMdnsSocketProvider(any(), any(), any(), any());
doReturn(DEFAULT_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF).when(mDeps).getDeviceConfigInt(
eq(NsdService.MDNS_CONFIG_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF), anyInt());
- doReturn(mAdvertiser).when(mDeps).makeMdnsAdvertiser(any(), any(), any(), any(), any());
+ doReturn(mAdvertiser).when(mDeps).makeMdnsAdvertiser(any(), any(), any(), any(), any(),
+ any());
doReturn(mMetrics).when(mDeps).makeNetworkNsdReportedMetrics(anyInt());
doReturn(mClock).when(mDeps).makeClock();
doReturn(TEST_TIME_MS).when(mClock).elapsedRealtime();
@@ -256,6 +265,10 @@
mThread.quitSafely();
mThread.join();
}
+
+ // Clear inline mocks as there are possible memory leaks if not done (see mockito
+ // doc for clearInlineMocks), and some tests create many of them.
+ Mockito.framework().clearInlineMocks();
}
// Native mdns provided by Netd is removed after U.
@@ -508,6 +521,56 @@
}
@Test
+ @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ public void testDiscoverOnTetheringDownstream_DiscoveryManager() throws Exception {
+ final NsdManager client = connectClient(mService);
+ final DiscoveryListener discListener = mock(DiscoveryListener.class);
+ client.discoverServices(SERVICE_TYPE, PROTOCOL, discListener);
+ waitForIdle();
+
+ final ArgumentCaptor<MdnsServiceBrowserListener> discoverListenerCaptor =
+ ArgumentCaptor.forClass(MdnsServiceBrowserListener.class);
+ final InOrder discManagerOrder = inOrder(mDiscoveryManager);
+ final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
+ discManagerOrder.verify(mDiscoveryManager).registerListener(eq(serviceTypeWithLocalDomain),
+ discoverListenerCaptor.capture(), any());
+
+ final int interfaceIdx = 123;
+ final MdnsServiceInfo mockServiceInfo = new MdnsServiceInfo(
+ SERVICE_NAME, /* serviceInstanceName */
+ serviceTypeWithLocalDomain.split("\\."), /* serviceType */
+ List.of(), /* subtypes */
+ new String[] {"android", "local"}, /* hostName */
+ 12345, /* port */
+ List.of(IPV4_ADDRESS),
+ List.of(IPV6_ADDRESS),
+ List.of(), /* textStrings */
+ List.of(), /* textEntries */
+ interfaceIdx, /* interfaceIndex */
+ null /* network */,
+ Instant.MAX /* expirationTime */);
+
+ // Verify service is found with the interface index
+ discoverListenerCaptor.getValue().onServiceNameDiscovered(
+ mockServiceInfo, false /* isServiceFromCache */);
+ final ArgumentCaptor<NsdServiceInfo> foundInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ verify(discListener, timeout(TIMEOUT_MS)).onServiceFound(foundInfoCaptor.capture());
+ final NsdServiceInfo foundInfo = foundInfoCaptor.getValue();
+ assertNull(foundInfo.getNetwork());
+ assertEquals(interfaceIdx, foundInfo.getInterfaceIndex());
+
+ // Using the returned service info to resolve or register callback uses the interface index
+ client.resolveService(foundInfo, mock(ResolveListener.class));
+ client.registerServiceInfoCallback(foundInfo, Runnable::run,
+ mock(ServiceInfoCallback.class));
+ waitForIdle();
+
+ discManagerOrder.verify(mDiscoveryManager, times(2)).registerListener(any(), any(), argThat(
+ o -> o.getNetwork() == null && o.getInterfaceIndex() == interfaceIdx));
+ }
+
+ @Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
@DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testDiscoverOnBlackholeNetwork() throws Exception {
@@ -716,6 +779,86 @@
true /* isLegacy */, getAddrId, 10L /* durationMs */);
}
+ @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @Test
+ public void testPerClientListenerLimit() throws Exception {
+ final NsdManager client1 = connectClient(mService);
+ final NsdManager client2 = connectClient(mService);
+
+ final String testType1 = "_testtype1._tcp";
+ final NsdServiceInfo testServiceInfo1 = new NsdServiceInfo("MyTestService1", testType1);
+ testServiceInfo1.setPort(12345);
+ final String testType2 = "_testtype2._tcp";
+ final NsdServiceInfo testServiceInfo2 = new NsdServiceInfo("MyTestService2", testType2);
+ testServiceInfo2.setPort(12345);
+
+ // Each client can register 200 requests (for example 100 discover and 100 register).
+ final int numEachListener = 100;
+ final ArrayList<DiscoveryListener> discListeners = new ArrayList<>(numEachListener);
+ final ArrayList<RegistrationListener> regListeners = new ArrayList<>(numEachListener);
+ for (int i = 0; i < numEachListener; i++) {
+ final DiscoveryListener discListener1 = mock(DiscoveryListener.class);
+ discListeners.add(discListener1);
+ final RegistrationListener regListener1 = mock(RegistrationListener.class);
+ regListeners.add(regListener1);
+ final DiscoveryListener discListener2 = mock(DiscoveryListener.class);
+ discListeners.add(discListener2);
+ final RegistrationListener regListener2 = mock(RegistrationListener.class);
+ regListeners.add(regListener2);
+ client1.discoverServices(testType1, NsdManager.PROTOCOL_DNS_SD,
+ (Network) null, Runnable::run, discListener1);
+ client1.registerService(testServiceInfo1, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+ regListener1);
+
+ client2.registerService(testServiceInfo2, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+ regListener2);
+ client2.discoverServices(testType2, NsdManager.PROTOCOL_DNS_SD,
+ (Network) null, Runnable::run, discListener2);
+ }
+
+ // Use a longer timeout than usual for the handler to process all the events. The
+ // registrations take about 1s on a high-end 2013 device.
+ HandlerUtils.waitForIdle(mHandler, 30_000L);
+ for (int i = 0; i < discListeners.size(); i++) {
+ // Callbacks are sent on the manager handler which is different from mHandler, so use
+ // a short timeout (each callback should come quickly after the previous one).
+ verify(discListeners.get(i), timeout(TEST_TIME_MS))
+ .onDiscoveryStarted(i % 2 == 0 ? testType1 : testType2);
+
+ // registerService does not get a callback before probing finishes (will not happen as
+ // this is mocked)
+ verifyNoMoreInteractions(regListeners.get(i));
+ }
+
+ // The next registrations should fail
+ final DiscoveryListener failDiscListener1 = mock(DiscoveryListener.class);
+ final RegistrationListener failRegListener1 = mock(RegistrationListener.class);
+ final DiscoveryListener failDiscListener2 = mock(DiscoveryListener.class);
+ final RegistrationListener failRegListener2 = mock(RegistrationListener.class);
+
+ client1.discoverServices(testType1, NsdManager.PROTOCOL_DNS_SD,
+ (Network) null, Runnable::run, failDiscListener1);
+ verify(failDiscListener1, timeout(TEST_TIME_MS))
+ .onStartDiscoveryFailed(testType1, FAILURE_MAX_LIMIT);
+
+ client1.registerService(testServiceInfo1, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+ failRegListener1);
+ verify(failRegListener1, timeout(TEST_TIME_MS)).onRegistrationFailed(
+ argThat(a -> testServiceInfo1.getServiceName().equals(a.getServiceName())),
+ eq(FAILURE_MAX_LIMIT));
+
+ client1.discoverServices(testType2, NsdManager.PROTOCOL_DNS_SD,
+ (Network) null, Runnable::run, failDiscListener2);
+ verify(failDiscListener2, timeout(TEST_TIME_MS))
+ .onStartDiscoveryFailed(testType2, FAILURE_MAX_LIMIT);
+
+ client1.registerService(testServiceInfo2, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+ failRegListener2);
+ verify(failRegListener2, timeout(TEST_TIME_MS)).onRegistrationFailed(
+ argThat(a -> testServiceInfo2.getServiceName().equals(a.getServiceName())),
+ eq(FAILURE_MAX_LIMIT));
+ }
+
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
@DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@@ -883,7 +1026,8 @@
List.of() /* textStrings */,
List.of() /* textEntries */,
1234,
- network);
+ network,
+ Instant.MAX /* expirationTime */);
// Callbacks for query sent.
listener.onDiscoveryQuerySent(Collections.emptyList(), 1 /* transactionId */);
@@ -913,7 +1057,8 @@
List.of() /* textStrings */,
List.of() /* textEntries */,
1234,
- network);
+ network,
+ Instant.MAX /* expirationTime */);
// Verify onServiceUpdated callback.
listener.onServiceUpdated(updatedServiceInfo);
@@ -1045,7 +1190,8 @@
List.of(), /* textStrings */
List.of(), /* textEntries */
1234, /* interfaceIndex */
- network);
+ network,
+ Instant.MAX /* expirationTime */);
// Verify onServiceNameDiscovered callback
listener.onServiceNameDiscovered(foundInfo, false /* isServiceFromCache */);
@@ -1066,7 +1212,8 @@
null, /* textStrings */
null, /* textEntries */
1234, /* interfaceIndex */
- network);
+ network,
+ Instant.MAX /* expirationTime */);
// Verify onServiceNameRemoved callback
listener.onServiceNameRemoved(removedInfo);
verify(discListener, timeout(TIMEOUT_MS)).onServiceLost(argThat(info ->
@@ -1138,7 +1285,7 @@
verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(s ->
"Instance".equals(s.getServiceName())
&& SERVICE_TYPE.equals(s.getServiceType())
- && s.getSubtypes().equals(Set.of("_subtype"))), any());
+ && s.getSubtypes().equals(Set.of("_subtype"))), any(), anyInt());
final DiscoveryListener discListener = mock(DiscoveryListener.class);
client.discoverServices(typeWithSubtype, PROTOCOL, network, Runnable::run, discListener);
@@ -1188,7 +1335,8 @@
List.of(MdnsServiceInfo.TextEntry.fromBytes(new byte[]{
'k', 'e', 'y', '=', (byte) 0xFF, (byte) 0xFE})) /* textEntries */,
1234,
- network);
+ network,
+ Instant.ofEpochSecond(1000_000L) /* expirationTime */);
// Verify onServiceFound callback
doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
@@ -1213,6 +1361,7 @@
assertTrue(info.getHostAddresses().stream().anyMatch(
address -> address.equals(parseNumericAddress("2001:db8::2"))));
assertEquals(network, info.getNetwork());
+ assertEquals(Instant.ofEpochSecond(1000_000L), info.getExpirationTime());
// Verify the listener has been unregistered.
verify(mDiscoveryManager, timeout(TIMEOUT_MS))
@@ -1245,7 +1394,7 @@
final ArgumentCaptor<Integer> serviceIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addOrUpdateService(serviceIdCaptor.capture(),
- argThat(info -> matches(info, regInfo)), any());
+ argThat(info -> matches(info, regInfo)), any(), anyInt());
client.unregisterService(regListenerWithoutFeature);
waitForIdle();
@@ -1274,7 +1423,7 @@
service1.setHostAddresses(List.of(parseNumericAddress("2001:db8::123")));
service1.setPort(1234);
final NsdServiceInfo service2 = new NsdServiceInfo(SERVICE_NAME, "_type2._tcp");
- service2.setHostAddresses(List.of(parseNumericAddress("2001:db8::123")));
+ service1.setHostAddresses(List.of(parseNumericAddress("2001:db8::123")));
service2.setPort(1234);
client.discoverServices(service1.getServiceType(),
@@ -1306,9 +1455,9 @@
// The advertiser is enabled for _type2 but not _type1
verify(mAdvertiser, never()).addOrUpdateService(anyInt(),
- argThat(info -> matches(info, service1)), any());
+ argThat(info -> matches(info, service1)), any(), anyInt());
verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(info -> matches(info, service2)),
- any());
+ any(), anyInt());
}
@Test
@@ -1320,7 +1469,7 @@
// final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
- verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any());
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
regInfo.setHost(parseNumericAddress("192.0.2.123"));
@@ -1333,7 +1482,7 @@
verify(mSocketProvider).startMonitoringSockets();
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addOrUpdateService(idCaptor.capture(), argThat(info ->
- matches(info, regInfo)), any());
+ matches(info, regInfo)), any(), anyInt());
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1371,7 +1520,7 @@
// final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
- verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any());
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, "invalid_type");
regInfo.setHost(parseNumericAddress("192.0.2.123"));
@@ -1381,7 +1530,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
- verify(mAdvertiser, never()).addOrUpdateService(anyInt(), any(), any());
+ verify(mAdvertiser, never()).addOrUpdateService(anyInt(), any(), any(), anyInt());
verify(regListener, timeout(TIMEOUT_MS)).onRegistrationFailed(
argThat(info -> matches(info, regInfo)), eq(FAILURE_INTERNAL_ERROR));
@@ -1398,7 +1547,7 @@
// final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
- verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any());
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
final NsdServiceInfo regInfo = new NsdServiceInfo("a".repeat(70), SERVICE_TYPE);
regInfo.setHost(parseNumericAddress("192.0.2.123"));
@@ -1410,8 +1559,12 @@
waitForIdle();
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
// Service name is truncated to 63 characters
- verify(mAdvertiser).addOrUpdateService(idCaptor.capture(),
- argThat(info -> info.getServiceName().equals("a".repeat(63))), any());
+ verify(mAdvertiser)
+ .addOrUpdateService(
+ idCaptor.capture(),
+ argThat(info -> info.getServiceName().equals("a".repeat(63))),
+ any(),
+ anyInt());
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1426,6 +1579,82 @@
}
@Test
+ public void testAdvertiseCustomTtl_validTtl_success() {
+ runValidTtlAdvertisingTest(30L);
+ runValidTtlAdvertisingTest(10 * 3600L);
+ }
+
+ @Test
+ public void testAdvertiseCustomTtl_ttlSmallerThan30SecondsButClientIsSystemServer_success() {
+ when(mDeps.getCallingUid()).thenReturn(Process.SYSTEM_UID);
+
+ runValidTtlAdvertisingTest(29L);
+ }
+
+ @Test
+ public void testAdvertiseCustomTtl_ttlLargerThan10HoursButClientIsSystemServer_success() {
+ when(mDeps.getCallingUid()).thenReturn(Process.SYSTEM_UID);
+
+ runValidTtlAdvertisingTest(10 * 3600L + 1);
+ runValidTtlAdvertisingTest(0xffffffffL);
+ }
+
+ private void runValidTtlAdvertisingTest(long validTtlSeconds) {
+ setMdnsAdvertiserEnabled();
+
+ final NsdManager client = connectClient(mService);
+ final RegistrationListener regListener = mock(RegistrationListener.class);
+ final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+ ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
+
+ final NsdServiceInfo regInfo = new NsdServiceInfo("Service custom TTL", SERVICE_TYPE);
+ regInfo.setPort(1234);
+ final AdvertisingRequest request =
+ new AdvertisingRequest.Builder(regInfo, NsdManager.PROTOCOL_DNS_SD)
+ .setTtl(Duration.ofSeconds(validTtlSeconds)).build();
+
+ client.registerService(request, Runnable::run, regListener);
+ waitForIdle();
+
+ final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
+ final MdnsAdvertisingOptions expectedAdverstingOptions =
+ MdnsAdvertisingOptions.newBuilder().setTtl(request.getTtl()).build();
+ verify(mAdvertiser).addOrUpdateService(idCaptor.capture(), any(),
+ eq(expectedAdverstingOptions), anyInt());
+
+ // Verify onServiceRegistered callback
+ final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
+ final int regId = idCaptor.getValue();
+ cb.onRegisterServiceSucceeded(regId, regInfo);
+
+ verify(regListener, timeout(TIMEOUT_MS)).onServiceRegistered(
+ argThat(info -> matches(info, new NsdServiceInfo(regInfo.getServiceName(), null))));
+ }
+
+ @Test
+ public void testAdvertiseCustomTtl_invalidTtl_FailsWithBadParameters() {
+ setMdnsAdvertiserEnabled();
+ final long invalidTtlSeconds = 29L;
+ final NsdManager client = connectClient(mService);
+ final RegistrationListener regListener = mock(RegistrationListener.class);
+ final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+ ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
+
+ final NsdServiceInfo regInfo = new NsdServiceInfo("Service custom TTL", SERVICE_TYPE);
+ regInfo.setPort(1234);
+ final AdvertisingRequest request =
+ new AdvertisingRequest.Builder(regInfo, NsdManager.PROTOCOL_DNS_SD)
+ .setTtl(Duration.ofSeconds(invalidTtlSeconds)).build();
+ client.registerService(request, Runnable::run, regListener);
+ waitForIdle();
+
+ verify(regListener, timeout(TIMEOUT_MS))
+ .onRegistrationFailed(any(), eq(FAILURE_BAD_PARAMETERS));
+ }
+
+ @Test
public void testStopServiceResolutionWithMdnsDiscoveryManager() {
setMdnsDiscoveryManagerEnabled();
@@ -1509,7 +1738,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
- verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any());
+ verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), anyInt());
// Verify the discovery uses MdnsDiscoveryManager
final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -1542,7 +1771,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
- verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any());
+ verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), anyInt());
final Network wifiNetwork1 = new Network(123);
final Network wifiNetwork2 = new Network(124);
diff --git a/tests/unit/java/com/android/server/VpnManagerServiceTest.java b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
deleted file mode 100644
index bf23cd1..0000000
--- a/tests/unit/java/com/android/server/VpnManagerServiceTest.java
+++ /dev/null
@@ -1,409 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server;
-
-import static android.os.Build.VERSION_CODES.R;
-
-import static com.android.testutils.ContextUtils.mockService;
-import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import static com.android.testutils.MiscAsserts.assertThrows;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.annotation.UserIdInt;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.net.ConnectivityManager;
-import android.net.INetd;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.INetworkManagementService;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.security.Credentials;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.net.VpnProfile;
-import com.android.server.connectivity.Vpn;
-import com.android.server.connectivity.VpnProfileStore;
-import com.android.server.net.LockdownVpnTracker;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-import com.android.testutils.HandlerUtils;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-
-@RunWith(DevSdkIgnoreRunner.class)
-@IgnoreUpTo(R) // VpnManagerService is not available before R
-@SmallTest
-public class VpnManagerServiceTest extends VpnTestBase {
- private static final String CONTEXT_ATTRIBUTION_TAG = "VPN_MANAGER";
-
- @Rule
- public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
-
- private static final int TIMEOUT_MS = 2_000;
-
- @Mock Context mContext;
- @Mock Context mContextWithoutAttributionTag;
- @Mock Context mSystemContext;
- @Mock Context mUserAllContext;
- private HandlerThread mHandlerThread;
- @Mock private Vpn mVpn;
- @Mock private INetworkManagementService mNms;
- @Mock private ConnectivityManager mCm;
- @Mock private UserManager mUserManager;
- @Mock private INetd mNetd;
- @Mock private PackageManager mPackageManager;
- @Mock private VpnProfileStore mVpnProfileStore;
- @Mock private LockdownVpnTracker mLockdownVpnTracker;
-
- private VpnManagerServiceDependencies mDeps;
- private VpnManagerService mService;
- private BroadcastReceiver mUserPresentReceiver;
- private BroadcastReceiver mIntentReceiver;
- private final String mNotMyVpnPkg = "com.not.my.vpn";
-
- class VpnManagerServiceDependencies extends VpnManagerService.Dependencies {
- @Override
- public HandlerThread makeHandlerThread() {
- return mHandlerThread;
- }
-
- @Override
- public INetworkManagementService getINetworkManagementService() {
- return mNms;
- }
-
- @Override
- public INetd getNetd() {
- return mNetd;
- }
-
- @Override
- public Vpn createVpn(Looper looper, Context context, INetworkManagementService nms,
- INetd netd, @UserIdInt int userId) {
- return mVpn;
- }
-
- @Override
- public VpnProfileStore getVpnProfileStore() {
- return mVpnProfileStore;
- }
-
- @Override
- public LockdownVpnTracker createLockDownVpnTracker(Context context, Handler handler,
- Vpn vpn, VpnProfile profile) {
- return mLockdownVpnTracker;
- }
-
- @Override
- public @UserIdInt int getMainUserId() {
- return UserHandle.USER_SYSTEM;
- }
- }
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
-
- mHandlerThread = new HandlerThread("TestVpnManagerService");
- mDeps = new VpnManagerServiceDependencies();
-
- // The attribution tag is a dependency for IKE library to collect VPN metrics correctly
- // and thus should not be changed without updating the IKE code.
- doReturn(mContext)
- .when(mContextWithoutAttributionTag)
- .createAttributionContext(CONTEXT_ATTRIBUTION_TAG);
-
- doReturn(mUserAllContext).when(mContext).createContextAsUser(UserHandle.ALL, 0);
- doReturn(mSystemContext).when(mContext).createContextAsUser(UserHandle.SYSTEM, 0);
- doReturn(mPackageManager).when(mContext).getPackageManager();
- setMockedPackages(mPackageManager, sPackages);
-
- mockService(mContext, ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mCm);
- mockService(mContext, UserManager.class, Context.USER_SERVICE, mUserManager);
- doReturn(SYSTEM_USER).when(mUserManager).getUserInfo(eq(SYSTEM_USER_ID));
-
- mService = new VpnManagerService(mContextWithoutAttributionTag, mDeps);
- mService.systemReady();
-
- final ArgumentCaptor<BroadcastReceiver> intentReceiverCaptor =
- ArgumentCaptor.forClass(BroadcastReceiver.class);
- final ArgumentCaptor<BroadcastReceiver> userPresentReceiverCaptor =
- ArgumentCaptor.forClass(BroadcastReceiver.class);
- verify(mSystemContext).registerReceiver(
- userPresentReceiverCaptor.capture(), any(), any(), any());
- verify(mUserAllContext, times(2)).registerReceiver(
- intentReceiverCaptor.capture(), any(), any(), any());
- mUserPresentReceiver = userPresentReceiverCaptor.getValue();
- mIntentReceiver = intentReceiverCaptor.getValue();
-
- // Add user to create vpn in mVpn
- onUserStarted(SYSTEM_USER_ID);
- assertNotNull(mService.mVpns.get(SYSTEM_USER_ID));
- }
-
- @Test
- public void testUpdateAppExclusionList() {
- // Start vpn
- mService.startVpnProfile(TEST_VPN_PKG);
- verify(mVpn).startVpnProfile(eq(TEST_VPN_PKG));
-
- // Remove package due to package replaced.
- onPackageRemoved(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
- verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
-
- // Add package due to package replaced.
- onPackageAdded(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
- verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
-
- // Remove package
- onPackageRemoved(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
- verify(mVpn).refreshPlatformVpnAppExclusionList();
-
- // Add the package back
- onPackageAdded(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
- verify(mVpn, times(2)).refreshPlatformVpnAppExclusionList();
- }
-
- @Test
- public void testStartVpnProfileFromDiffPackage() {
- assertThrows(
- SecurityException.class, () -> mService.startVpnProfile(mNotMyVpnPkg));
- }
-
- @Test
- public void testStopVpnProfileFromDiffPackage() {
- assertThrows(SecurityException.class, () -> mService.stopVpnProfile(mNotMyVpnPkg));
- }
-
- @Test
- public void testGetProvisionedVpnProfileStateFromDiffPackage() {
- assertThrows(SecurityException.class, () ->
- mService.getProvisionedVpnProfileState(mNotMyVpnPkg));
- }
-
- @Test
- public void testGetProvisionedVpnProfileState() {
- mService.getProvisionedVpnProfileState(TEST_VPN_PKG);
- verify(mVpn).getProvisionedVpnProfileState(TEST_VPN_PKG);
- }
-
- private Intent buildIntent(String action, String packageName, int userId, int uid,
- boolean isReplacing) {
- final Intent intent = new Intent(action);
- intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
- intent.putExtra(Intent.EXTRA_UID, uid);
- intent.putExtra(Intent.EXTRA_REPLACING, isReplacing);
- if (packageName != null) {
- intent.setData(Uri.fromParts("package" /* scheme */, packageName, null /* fragment */));
- }
-
- return intent;
- }
-
- private void sendIntent(Intent intent) {
- sendIntent(mIntentReceiver, mContext, intent);
- }
-
- private void sendIntent(BroadcastReceiver receiver, Context context, Intent intent) {
- final Handler h = mHandlerThread.getThreadHandler();
-
- // Send in handler thread.
- h.post(() -> receiver.onReceive(context, intent));
- HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
- }
-
- private void onUserStarted(int userId) {
- sendIntent(buildIntent(Intent.ACTION_USER_STARTED,
- null /* packageName */, userId, -1 /* uid */, false /* isReplacing */));
- }
-
- private void onUserUnlocked(int userId) {
- sendIntent(buildIntent(Intent.ACTION_USER_UNLOCKED,
- null /* packageName */, userId, -1 /* uid */, false /* isReplacing */));
- }
-
- private void onUserStopped(int userId) {
- sendIntent(buildIntent(Intent.ACTION_USER_STOPPED,
- null /* packageName */, userId, -1 /* uid */, false /* isReplacing */));
- }
-
- private void onLockDownReset() {
- sendIntent(buildIntent(LockdownVpnTracker.ACTION_LOCKDOWN_RESET, null /* packageName */,
- UserHandle.USER_SYSTEM, -1 /* uid */, false /* isReplacing */));
- }
-
- private void onPackageAdded(String packageName, int userId, int uid, boolean isReplacing) {
- sendIntent(buildIntent(Intent.ACTION_PACKAGE_ADDED, packageName, userId, uid, isReplacing));
- }
-
- private void onPackageAdded(String packageName, int uid, boolean isReplacing) {
- onPackageAdded(packageName, UserHandle.USER_SYSTEM, uid, isReplacing);
- }
-
- private void onPackageRemoved(String packageName, int userId, int uid, boolean isReplacing) {
- sendIntent(buildIntent(Intent.ACTION_PACKAGE_REMOVED, packageName, userId, uid,
- isReplacing));
- }
-
- private void onPackageRemoved(String packageName, int uid, boolean isReplacing) {
- onPackageRemoved(packageName, UserHandle.USER_SYSTEM, uid, isReplacing);
- }
-
- @Test
- public void testReceiveIntentFromNonHandlerThread() {
- assertThrows(IllegalStateException.class, () ->
- mIntentReceiver.onReceive(mContext, buildIntent(Intent.ACTION_PACKAGE_REMOVED,
- PKGS[0], UserHandle.USER_SYSTEM, PKG_UIDS[0], true /* isReplacing */)));
-
- assertThrows(IllegalStateException.class, () ->
- mUserPresentReceiver.onReceive(mContext, new Intent(Intent.ACTION_USER_PRESENT)));
- }
-
- private void setupLockdownVpn(String packageName) {
- final byte[] profileTag = packageName.getBytes(StandardCharsets.UTF_8);
- doReturn(profileTag).when(mVpnProfileStore).get(Credentials.LOCKDOWN_VPN);
- }
-
- private void setupVpnProfile(String profileName) {
- final VpnProfile profile = new VpnProfile(profileName);
- profile.name = profileName;
- profile.server = "192.0.2.1";
- profile.dnsServers = "8.8.8.8";
- profile.type = VpnProfile.TYPE_IPSEC_XAUTH_PSK;
- final byte[] encodedProfile = profile.encode();
- doReturn(encodedProfile).when(mVpnProfileStore).get(Credentials.VPN + profileName);
- }
-
- @Test
- public void testUserPresent() {
- // Verify that LockDownVpnTracker is not created.
- verify(mLockdownVpnTracker, never()).init();
-
- setupLockdownVpn(TEST_VPN_PKG);
- setupVpnProfile(TEST_VPN_PKG);
-
- // mUserPresentReceiver only registers ACTION_USER_PRESENT intent and does no verification
- // on action, so an empty intent is enough.
- sendIntent(mUserPresentReceiver, mSystemContext, new Intent());
-
- verify(mLockdownVpnTracker).init();
- verify(mSystemContext).unregisterReceiver(mUserPresentReceiver);
- verify(mUserAllContext, never()).unregisterReceiver(any());
- }
-
- @Test
- public void testUpdateLockdownVpn() {
- setupLockdownVpn(TEST_VPN_PKG);
- onUserUnlocked(SYSTEM_USER_ID);
-
- // Will not create lockDownVpnTracker w/o valid profile configured in the keystore
- verify(mLockdownVpnTracker, never()).init();
-
- setupVpnProfile(TEST_VPN_PKG);
-
- // Remove the user from mVpns
- onUserStopped(SYSTEM_USER_ID);
- onUserUnlocked(SYSTEM_USER_ID);
- verify(mLockdownVpnTracker, never()).init();
-
- // Add user back
- onUserStarted(SYSTEM_USER_ID);
- verify(mLockdownVpnTracker).init();
-
- // Trigger another update. The existing LockDownVpnTracker should be shut down and
- // initialize another one.
- onUserUnlocked(SYSTEM_USER_ID);
- verify(mLockdownVpnTracker).shutdown();
- verify(mLockdownVpnTracker, times(2)).init();
- }
-
- @Test
- public void testLockdownReset() {
- // Init LockdownVpnTracker
- setupLockdownVpn(TEST_VPN_PKG);
- setupVpnProfile(TEST_VPN_PKG);
- onUserUnlocked(SYSTEM_USER_ID);
- verify(mLockdownVpnTracker).init();
-
- onLockDownReset();
- verify(mLockdownVpnTracker).reset();
- }
-
- @Test
- public void testLockdownResetWhenLockdownVpnTrackerIsNotInit() {
- setupLockdownVpn(TEST_VPN_PKG);
- setupVpnProfile(TEST_VPN_PKG);
-
- onLockDownReset();
-
- // LockDownVpnTracker is not created. Lockdown reset will not take effect.
- verify(mLockdownVpnTracker, never()).reset();
- }
-
- @Test
- public void testIsVpnLockdownEnabled() {
- // Vpn is created but the VPN lockdown is not enabled.
- assertFalse(mService.isVpnLockdownEnabled(SYSTEM_USER_ID));
-
- // Set lockdown for the SYSTEM_USER_ID VPN.
- doReturn(true).when(mVpn).getLockdown();
- assertTrue(mService.isVpnLockdownEnabled(SYSTEM_USER_ID));
-
- // Even lockdown is enabled but no Vpn is created for SECONDARY_USER.
- assertFalse(mService.isVpnLockdownEnabled(SECONDARY_USER.id));
- }
-
- @Test
- public void testGetVpnLockdownAllowlist() {
- doReturn(null).when(mVpn).getLockdownAllowlist();
- assertNull(mService.getVpnLockdownAllowlist(SYSTEM_USER_ID));
-
- final List<String> expected = List.of(PKGS);
- doReturn(expected).when(mVpn).getLockdownAllowlist();
- assertEquals(expected, mService.getVpnLockdownAllowlist(SYSTEM_USER_ID));
-
- // Even lockdown is enabled but no Vpn is created for SECONDARY_USER.
- assertNull(mService.getVpnLockdownAllowlist(SECONDARY_USER.id));
- }
-}
diff --git a/tests/unit/java/com/android/server/VpnTestBase.java b/tests/unit/java/com/android/server/VpnTestBase.java
deleted file mode 100644
index 6113872..0000000
--- a/tests/unit/java/com/android/server/VpnTestBase.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server;
-
-import static android.content.pm.UserInfo.FLAG_ADMIN;
-import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
-import static android.content.pm.UserInfo.FLAG_PRIMARY;
-import static android.content.pm.UserInfo.FLAG_RESTRICTED;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doAnswer;
-
-import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
-import android.os.Process;
-import android.os.UserHandle;
-import android.util.ArrayMap;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-/** Common variables or methods shared between VpnTest and VpnManagerServiceTest. */
-public class VpnTestBase {
- protected static final String TEST_VPN_PKG = "com.testvpn.vpn";
- /**
- * Names and UIDs for some fake packages. Important points:
- * - UID is ordered increasing.
- * - One pair of packages have consecutive UIDs.
- */
- protected static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"};
- protected static final int[] PKG_UIDS = {10066, 10077, 10078, 10400};
- // Mock packages
- protected static final Map<String, Integer> sPackages = new ArrayMap<>();
- static {
- for (int i = 0; i < PKGS.length; i++) {
- sPackages.put(PKGS[i], PKG_UIDS[i]);
- }
- sPackages.put(TEST_VPN_PKG, Process.myUid());
- }
-
- // Mock users
- protected static final int SYSTEM_USER_ID = 0;
- protected static final UserInfo SYSTEM_USER = new UserInfo(0, "system", UserInfo.FLAG_PRIMARY);
- protected static final UserInfo PRIMARY_USER = new UserInfo(27, "Primary",
- FLAG_ADMIN | FLAG_PRIMARY);
- protected static final UserInfo SECONDARY_USER = new UserInfo(15, "Secondary", FLAG_ADMIN);
- protected static final UserInfo RESTRICTED_PROFILE_A = new UserInfo(40, "RestrictedA",
- FLAG_RESTRICTED);
- protected static final UserInfo RESTRICTED_PROFILE_B = new UserInfo(42, "RestrictedB",
- FLAG_RESTRICTED);
- protected static final UserInfo MANAGED_PROFILE_A = new UserInfo(45, "ManagedA",
- FLAG_MANAGED_PROFILE);
- static {
- RESTRICTED_PROFILE_A.restrictedProfileParentId = PRIMARY_USER.id;
- RESTRICTED_PROFILE_B.restrictedProfileParentId = SECONDARY_USER.id;
- MANAGED_PROFILE_A.profileGroupId = PRIMARY_USER.id;
- }
-
- // Populate a fake packageName-to-UID mapping.
- protected void setMockedPackages(PackageManager mockPm, final Map<String, Integer> packages) {
- try {
- doAnswer(invocation -> {
- final String appName = (String) invocation.getArguments()[0];
- final int userId = (int) invocation.getArguments()[1];
-
- final Integer appId = packages.get(appName);
- if (appId == null) {
- throw new PackageManager.NameNotFoundException(appName);
- }
-
- return UserHandle.getUid(userId, appId);
- }).when(mockPm).getPackageUidAsUser(anyString(), anyInt());
- } catch (Exception e) {
- }
- }
-
- protected List<Integer> toList(int[] arr) {
- return Arrays.stream(arr).boxed().collect(Collectors.toList());
- }
-}
diff --git a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index 4fcf8a8..c53feee 100644
--- a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -20,10 +20,8 @@
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-
import static com.android.server.connectivity.AutomaticOnOffKeepaliveTracker.METRICS_COLLECTION_DURATION_MS;
import static com.android.testutils.HandlerUtils.visibleOnHandlerThread;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -71,31 +69,16 @@
import android.os.Message;
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;
-
+import androidx.test.filters.SmallTest;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker.AutomaticOnOffKeepalive;
import com.android.server.connectivity.KeepaliveTracker.KeepaliveInfo;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.HandlerUtils;
-
-import libcore.util.HexEncoding;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
import java.io.FileDescriptor;
import java.io.StringWriter;
import java.net.Inet4Address;
@@ -104,9 +87,15 @@
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
-import java.util.Set;
+import libcore.util.HexEncoding;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@@ -236,9 +225,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,38 +402,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)));
- }
-
- @Test
- public void testIsAnyTcpSocketConnected_noTargetUidSocket() throws Exception {
- setupResponseWithSocketExisting();
- // Configured uid(12345) is not in the VPN range.
- assertFalse(visibleOnHandlerThread(mTestHandler,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(
- TEST_NETID,
- new ArraySet<>(Arrays.asList(new Range<>(99999, 99999))))));
+ () -> 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) {
@@ -491,16 +467,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/CarrierPrivilegeAuthenticatorTest.java b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
index f07593e..ab81abc 100644
--- a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
@@ -21,6 +21,7 @@
import static android.telephony.TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED;
import static com.android.server.connectivity.ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK;
+import static com.android.testutils.HandlerUtils.visibleOnHandlerThread;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -45,8 +46,8 @@
import android.net.NetworkCapabilities;
import android.net.TelephonyNetworkSpecifier;
import android.os.Build;
+import android.os.Handler;
import android.os.HandlerThread;
-import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import com.android.net.module.util.CollectionUtils;
@@ -54,10 +55,13 @@
import com.android.networkstack.apishim.common.TelephonyManagerShim.CarrierPrivilegesListenerShim;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import com.android.server.connectivity.CarrierPrivilegeAuthenticator.Dependencies;
+import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
import org.junit.After;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -67,6 +71,8 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
/**
* Tests for CarrierPrivilegeAuthenticatorTest.
@@ -77,35 +83,47 @@
@RunWith(DevSdkIgnoreRunner.class)
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class CarrierPrivilegeAuthenticatorTest {
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
private static final int SUBSCRIPTION_COUNT = 2;
private static final int TEST_SUBSCRIPTION_ID = 1;
+ private static final int TIMEOUT_MS = 1_000;
@NonNull private final Context mContext;
@NonNull private final TelephonyManager mTelephonyManager;
@NonNull private final TelephonyManagerShimImpl mTelephonyManagerShim;
@NonNull private final PackageManager mPackageManager;
@NonNull private TestCarrierPrivilegeAuthenticator mCarrierPrivilegeAuthenticator;
+ @NonNull private final BiConsumer<Integer, Integer> mListener;
private final int mCarrierConfigPkgUid = 12345;
+ private final boolean mUseCallbacks;
private final String mTestPkg = "com.android.server.connectivity.test";
private final BroadcastReceiver mMultiSimBroadcastReceiver;
@NonNull private final HandlerThread mHandlerThread;
+ @NonNull private final Handler mCsHandler;
+ @NonNull private final HandlerThread mCsHandlerThread;
public class TestCarrierPrivilegeAuthenticator extends CarrierPrivilegeAuthenticator {
TestCarrierPrivilegeAuthenticator(@NonNull final Context c,
@NonNull final Dependencies deps,
- @NonNull final TelephonyManager t) {
- super(c, deps, t, mTelephonyManagerShim);
+ @NonNull final TelephonyManager t,
+ @NonNull final Handler handler) {
+ super(c, deps, t, mTelephonyManagerShim, true /* requestRestrictedWifiEnabled */,
+ mListener, handler);
}
@Override
- protected int getSlotIndex(int subId) {
- if (SubscriptionManager.DEFAULT_SUBSCRIPTION_ID == subId) return TEST_SUBSCRIPTION_ID;
- return subId;
+ protected int getSubId(int slotIndex) {
+ return TEST_SUBSCRIPTION_ID;
}
}
@After
- public void tearDown() {
+ public void tearDown() throws Exception {
mHandlerThread.quit();
+ mHandlerThread.join();
+ mCsHandlerThread.quit();
+ mCsHandlerThread.join();
}
/** Parameters to test both using callbacks or the old broadcast */
@@ -119,7 +137,9 @@
mTelephonyManager = mock(TelephonyManager.class);
mTelephonyManagerShim = mock(TelephonyManagerShimImpl.class);
mPackageManager = mock(PackageManager.class);
+ mListener = mock(BiConsumer.class);
mHandlerThread = new HandlerThread(CarrierPrivilegeAuthenticatorTest.class.getSimpleName());
+ mUseCallbacks = useCallbacks;
final Dependencies deps = mock(Dependencies.class);
doReturn(useCallbacks).when(deps).isFeatureEnabled(any() /* context */,
eq(CARRIER_SERVICE_CHANGED_USE_CALLBACK));
@@ -131,8 +151,14 @@
final ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.uid = mCarrierConfigPkgUid;
doReturn(applicationInfo).when(mPackageManager).getApplicationInfo(eq(mTestPkg), anyInt());
- mCarrierPrivilegeAuthenticator =
- new TestCarrierPrivilegeAuthenticator(mContext, deps, mTelephonyManager);
+ mCsHandlerThread = new HandlerThread(
+ CarrierPrivilegeAuthenticatorTest.class.getSimpleName() + "-CsHandlerThread");
+ mCsHandlerThread.start();
+ mCsHandler = new Handler(mCsHandlerThread.getLooper());
+ mCarrierPrivilegeAuthenticator = new TestCarrierPrivilegeAuthenticator(mContext, deps,
+ mTelephonyManager, mCsHandler);
+ mCarrierPrivilegeAuthenticator.start();
+ HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
ArgumentCaptor.forClass(BroadcastReceiver.class);
verify(mContext).registerReceiver(receiverCaptor.capture(), argThat(filter ->
@@ -168,11 +194,13 @@
assertNotNull(initialListeners.get(1));
assertEquals(2, initialListeners.size());
- initialListeners.get(0).onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+ visibleOnHandlerThread(mCsHandler, () -> {
+ initialListeners.get(0).onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+ });
final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_CELLULAR)
- .setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
+ .setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID));
assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
@@ -191,10 +219,10 @@
doReturn(1).when(mTelephonyManager).getActiveModemCount();
- // This is a little bit cavalier in that the call to onReceive is not on the handler
- // thread that was specified in registerReceiver.
- // TODO : capture the handler and call this on it if this causes flakiness.
- mMultiSimBroadcastReceiver.onReceive(mContext, buildTestMultiSimConfigBroadcastIntent());
+ visibleOnHandlerThread(mCsHandler, () -> {
+ mMultiSimBroadcastReceiver.onReceive(mContext,
+ buildTestMultiSimConfigBroadcastIntent());
+ });
// Check all listeners have been removed
for (CarrierPrivilegesListenerShim listener : initialListeners.values()) {
verify(mTelephonyManagerShim).removeCarrierPrivilegesListener(eq(listener));
@@ -206,9 +234,12 @@
assertNotNull(newListeners.get(0));
assertEquals(1, newListeners.size());
- newListeners.get(0).onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+ visibleOnHandlerThread(mCsHandler, () -> {
+ newListeners.get(0).onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+ });
- final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(0);
+ final TelephonyNetworkSpecifier specifier =
+ new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID);
final NetworkCapabilities nc = new NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_CELLULAR)
.setNetworkSpecifier(specifier)
@@ -220,10 +251,32 @@
}
@Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testCarrierPrivilegesLostDueToCarrierServiceUpdate() throws Exception {
+ final CarrierPrivilegesListenerShim l = getCarrierPrivilegesListeners().get(0);
+
+ visibleOnHandlerThread(mCsHandler, () -> {
+ l.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+ l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 1);
+ });
+ if (mUseCallbacks) {
+ verify(mListener).accept(eq(mCarrierConfigPkgUid), eq(TEST_SUBSCRIPTION_ID));
+ }
+
+ visibleOnHandlerThread(mCsHandler, () -> {
+ l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 2);
+ });
+ if (mUseCallbacks) {
+ verify(mListener).accept(eq(mCarrierConfigPkgUid + 1), eq(TEST_SUBSCRIPTION_ID));
+ }
+ }
+
+ @Test
public void testOnCarrierPrivilegesChanged() throws Exception {
final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
- final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(0);
+ final TelephonyNetworkSpecifier specifier =
+ new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID);
final NetworkCapabilities nc = new NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_CELLULAR)
.setNetworkSpecifier(specifier)
@@ -232,8 +285,10 @@
final ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.uid = mCarrierConfigPkgUid + 1;
doReturn(applicationInfo).when(mPackageManager).getApplicationInfo(eq(mTestPkg), anyInt());
- listener.onCarrierPrivilegesChanged(Collections.emptyList(), new int[] {});
- listener.onCarrierServiceChanged(null, applicationInfo.uid);
+ visibleOnHandlerThread(mCsHandler, () -> {
+ listener.onCarrierPrivilegesChanged(Collections.emptyList(), new int[]{});
+ listener.onCarrierServiceChanged(null, applicationInfo.uid);
+ });
assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, nc));
@@ -244,14 +299,16 @@
@Test
public void testDefaultSubscription() throws Exception {
final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
- listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+ visibleOnHandlerThread(mCsHandler, () -> {
+ listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+ });
final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder();
ncBuilder.addTransportType(TRANSPORT_CELLULAR);
assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
- ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
+ ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID));
assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
@@ -260,7 +317,39 @@
ncBuilder.setNetworkSpecifier(null);
ncBuilder.removeTransportType(TRANSPORT_CELLULAR);
ncBuilder.addTransportType(TRANSPORT_WIFI);
- ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
+ ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID));
+ assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
+ mCarrierConfigPkgUid, ncBuilder.build()));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testNetworkCapabilitiesContainOneSubId() throws Exception {
+ final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
+ visibleOnHandlerThread(mCsHandler, () -> {
+ listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+ });
+
+ final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder();
+ ncBuilder.addTransportType(TRANSPORT_WIFI);
+ ncBuilder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+ ncBuilder.setSubscriptionIds(Set.of(TEST_SUBSCRIPTION_ID));
+ assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
+ mCarrierConfigPkgUid, ncBuilder.build()));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testNetworkCapabilitiesContainTwoSubIds() throws Exception {
+ final CarrierPrivilegesListenerShim listener = getCarrierPrivilegesListeners().get(0);
+ visibleOnHandlerThread(mCsHandler, () -> {
+ listener.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+ });
+
+ final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder();
+ ncBuilder.addTransportType(TRANSPORT_WIFI);
+ ncBuilder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+ ncBuilder.setSubscriptionIds(Set.of(0, 1));
assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
}
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
index 88044be..da7fda3 100644
--- a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -526,13 +526,13 @@
+ "v4: /192.0.0.46, v6: /2001:db8:0:b11::464, pfx96: /64:ff9b::, "
+ "pid: 10483, cookie: 27149", dumpStrings[0].trim());
assertEquals("Forwarding rules:", dumpStrings[1].trim());
- assertEquals("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif",
+ assertEquals("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif (packets bytes)",
dumpStrings[2].trim());
- assertEquals("1000 /64:ff9b::/96 /2001:db8:0:b11::464 -> /192.0.0.46 1001",
+ assertEquals("1000 /64:ff9b::/96 /2001:db8:0:b11::464 -> /192.0.0.46 1001 (0 0)",
dumpStrings[3].trim());
- assertEquals("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif",
+ assertEquals("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif (packets bytes)",
dumpStrings[4].trim());
- assertEquals("1001 /192.0.0.46 -> /2001:db8:0:b11::464 /64:ff9b::/96 1000 ether",
+ assertEquals("1001 /192.0.0.46 -> /2001:db8:0:b11::464 /64:ff9b::/96 1000 ether (0 0)",
dumpStrings[5].trim());
} else {
assertEquals(1, dumpStrings.length);
diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
index 52b05aa..ab1e467 100644
--- a/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
+++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
@@ -26,7 +26,6 @@
import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityLog;
import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.MULTIPLE;
import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.WIFI;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@@ -43,17 +42,14 @@
import android.net.metrics.ValidationProbeEvent;
import android.net.metrics.WakeupStats;
import android.os.Build;
-import android.test.suitebuilder.annotation.SmallTest;
-
+import androidx.test.filters.SmallTest;
import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
import java.util.Arrays;
import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
// TODO: instead of comparing textpb to textpb, parse textpb and compare proto to proto.
@RunWith(DevSdkIgnoreRunner.class)
diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
index 5881a8e..91626d2 100644
--- a/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
+++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
@@ -18,7 +18,6 @@
import static android.net.metrics.INetdEventListener.EVENT_GETADDRINFO;
import static android.net.metrics.INetdEventListener.EVENT_GETHOSTBYNAME;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;
@@ -50,14 +49,14 @@
import android.os.Parcelable;
import android.os.SystemClock;
import android.system.OsConstants;
-import android.test.suitebuilder.annotation.SmallTest;
import android.util.Base64;
-
+import androidx.test.filters.SmallTest;
import com.android.internal.util.BitUtils;
import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
-
+import java.io.PrintWriter;
+import java.io.StringWriter;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -65,9 +64,6 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.io.PrintWriter;
-import java.io.StringWriter;
-
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
diff --git a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
index 1b964e2..294dacb 100644
--- a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
@@ -1297,8 +1297,8 @@
assertTrue(mKeepaliveStatsTracker.allMetricsExpected(dailyKeepaliveInfoReported));
- // Write time after 26 hours.
- final int writeTime2 = 26 * 60 * 60 * 1000;
+ // Write time after 27 hours.
+ final int writeTime2 = 27 * 60 * 60 * 1000;
setElapsedRealtime(writeTime2);
visibleOnHandlerThread(mTestHandler, () -> mKeepaliveStatsTracker.writeAndResetMetrics());
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/NetdEventListenerServiceTest.java b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
index d667662..89e2a51 100644
--- a/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
@@ -18,9 +18,7 @@
import static android.net.metrics.INetdEventListener.EVENT_GETADDRINFO;
import static android.net.metrics.INetdEventListener.EVENT_GETHOSTBYNAME;
-
import static com.android.testutils.MiscAsserts.assertStringContains;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
@@ -34,27 +32,23 @@
import android.net.NetworkCapabilities;
import android.os.Build;
import android.system.OsConstants;
-import android.test.suitebuilder.annotation.SmallTest;
import android.util.Base64;
-
+import androidx.test.filters.SmallTest;
import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityLog;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
-
-import libcore.util.EmptyArray;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
+import libcore.util.EmptyArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
diff --git a/tests/unit/java/com/android/metrics/NetworkRequestStateInfoTest.java b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java
similarity index 84%
rename from tests/unit/java/com/android/metrics/NetworkRequestStateInfoTest.java
rename to tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java
index 5709ed1..44a645a 100644
--- a/tests/unit/java/com/android/metrics/NetworkRequestStateInfoTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.metrics;
+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;
@@ -60,25 +60,25 @@
// This call will be used to calculate NR received time
Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrStartTime);
- NetworkRequestStateInfo mNetworkRequestStateInfo = new NetworkRequestStateInfo(
+ NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
notMeteredWifiNetworkRequest, mDependencies);
// This call will be used to calculate NR removed time
Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrEndTime);
- mNetworkRequestStateInfo.setNetworkRequestRemoved();
+ networkRequestStateInfo.setNetworkRequestRemoved();
assertEquals(
nrEndTime - nrStartTime,
- mNetworkRequestStateInfo.getNetworkRequestDurationMillis());
- assertEquals(mNetworkRequestStateInfo.getNetworkRequestStateStatsType(),
+ networkRequestStateInfo.getNetworkRequestDurationMillis());
+ assertEquals(networkRequestStateInfo.getNetworkRequestStateStatsType(),
NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED);
}
@Test
public void testCheckInitialState() {
- NetworkRequestStateInfo mNetworkRequestStateInfo = new NetworkRequestStateInfo(
+ NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
new NetworkRequest(new NetworkCapabilities(), 0, 1, NetworkRequest.Type.REQUEST),
mDependencies);
- assertEquals(mNetworkRequestStateInfo.getNetworkRequestStateStatsType(),
+ assertEquals(networkRequestStateInfo.getNetworkRequestStateStatsType(),
NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED);
}
}
diff --git a/tests/unit/java/com/android/metrics/NetworkRequestStateStatsMetricsTest.java b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java
similarity index 60%
rename from tests/unit/java/com/android/metrics/NetworkRequestStateStatsMetricsTest.java
rename to tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java
index 17a0719..8dc0528 100644
--- a/tests/unit/java/com/android/metrics/NetworkRequestStateStatsMetricsTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-package com.android.metrics;
-
+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;
@@ -24,14 +23,18 @@
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.times;
+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;
@@ -53,7 +56,10 @@
@Mock
private NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
@Captor
- private ArgumentCaptor<NetworkRequestStateInfo> mNetworkRequestStateInfoCaptor;
+ private ArgumentCaptor<Handler> mHandlerCaptor;
+ @Captor
+ private ArgumentCaptor<Integer> mMessageWhatCaptor;
+
private NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
private HandlerThread mHandlerThread;
private static final int TEST_REQUEST_ID = 10;
@@ -74,6 +80,13 @@
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);
}
@@ -85,12 +98,13 @@
// This call will be used to calculate NR received time
Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrStartTime);
mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
- HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
- verify(mNRStateStatsDeps, times(1))
- .writeStats(mNetworkRequestStateInfoCaptor.capture());
+ ArgumentCaptor<NetworkRequestStateInfo> networkRequestStateInfoCaptor =
+ ArgumentCaptor.forClass(NetworkRequestStateInfo.class);
+ verify(mNRStateStatsDeps, timeout(TIMEOUT_MS))
+ .writeStats(networkRequestStateInfoCaptor.capture());
- NetworkRequestStateInfo nrStateInfoSent = mNetworkRequestStateInfoCaptor.getValue();
+ 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());
@@ -104,12 +118,11 @@
// This call will be used to calculate NR removed time
Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrEndTime);
mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST);
- HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
- verify(mNRStateStatsDeps, times(1))
- .writeStats(mNetworkRequestStateInfoCaptor.capture());
+ verify(mNRStateStatsDeps, timeout(TIMEOUT_MS))
+ .writeStats(networkRequestStateInfoCaptor.capture());
- nrStateInfoSent = mNetworkRequestStateInfoCaptor.getValue();
+ nrStateInfoSent = networkRequestStateInfoCaptor.getValue();
assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED,
nrStateInfoSent.getNetworkRequestStateStatsType());
assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId());
@@ -129,10 +142,9 @@
}
@Test
- public void testExistingNetworkRequestReceived() {
+ public void testNoMessagesWhenNetworkRequestReceived() {
mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
- HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
- verify(mNRStateStatsDeps, times(1))
+ verify(mNRStateStatsDeps, timeout(TIMEOUT_MS))
.writeStats(any(NetworkRequestStateInfo.class));
clearInvocations(mNRStateStatsDeps);
@@ -140,6 +152,46 @@
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/SatelliteAccessControllerTest.kt b/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
new file mode 100644
index 0000000..7885325
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt
@@ -0,0 +1,334 @@
+/*
+ * 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 android.Manifest
+import android.app.role.OnRoleHoldersChangedListener
+import android.app.role.RoleManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.ArraySet
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.isNull
+import org.mockito.Mockito.doReturn
+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
+
+private const val PRIMARY_USER = 0
+private const val SECONDARY_USER = 10
+private val PRIMARY_USER_HANDLE = UserHandle.of(PRIMARY_USER)
+private val SECONDARY_USER_HANDLE = UserHandle.of(SECONDARY_USER)
+
+// sms app names
+private const val SMS_APP1 = "sms_app_1"
+private const val SMS_APP2 = "sms_app_2"
+
+// sms app ids
+private const val SMS_APP_ID1 = 100
+private const val SMS_APP_ID2 = 101
+
+// UID for app1 and app2 on primary user
+// These app could become default sms app for user1
+private val PRIMARY_USER_SMS_APP_UID1 = UserHandle.getUid(PRIMARY_USER, SMS_APP_ID1)
+private val PRIMARY_USER_SMS_APP_UID2 = UserHandle.getUid(PRIMARY_USER, SMS_APP_ID2)
+
+// UID for app1 and app2 on secondary user
+// These app could become default sms app for user2
+private val SECONDARY_USER_SMS_APP_UID1 = UserHandle.getUid(SECONDARY_USER, SMS_APP_ID1)
+private val SECONDARY_USER_SMS_APP_UID2 = UserHandle.getUid(SECONDARY_USER, SMS_APP_ID2)
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class SatelliteAccessControllerTest {
+ private val context = mock(Context::class.java)
+ private val primaryUserContext = mock(Context::class.java)
+ private val secondaryUserContext = mock(Context::class.java)
+ private val mPackageManagerPrimaryUser = mock(PackageManager::class.java)
+ private val mPackageManagerSecondaryUser = mock(PackageManager::class.java)
+ private val mDeps = mock(SatelliteAccessController.Dependencies::class.java)
+ private val mCallback = mock(Consumer::class.java) as Consumer<Set<Int>>
+ private val userManager = mock(UserManager::class.java)
+ private val mHandler = Handler(Looper.getMainLooper())
+ private var mSatelliteAccessController =
+ SatelliteAccessController(context, mDeps, mCallback, mHandler)
+ private lateinit var mRoleHolderChangedListener: OnRoleHoldersChangedListener
+ private lateinit var mUserRemovedReceiver: BroadcastReceiver
+
+ private fun <T> mockService(name: String, clazz: Class<T>, service: T) {
+ doReturn(name).`when`(context).getSystemServiceName(clazz)
+ doReturn(service).`when`(context).getSystemService(name)
+ if (context.getSystemService(clazz) == null) {
+ // Test is using mockito-extended
+ doReturn(service).`when`(context).getSystemService(clazz)
+ }
+ }
+
+ @Before
+ @Throws(PackageManager.NameNotFoundException::class)
+ fun setup() {
+ doReturn(emptyList<UserHandle>()).`when`(userManager).getUserHandles(true)
+ mockService(Context.USER_SERVICE, UserManager::class.java, userManager)
+
+ doReturn(primaryUserContext).`when`(context).createContextAsUser(PRIMARY_USER_HANDLE, 0)
+ doReturn(mPackageManagerPrimaryUser).`when`(primaryUserContext).packageManager
+
+ doReturn(secondaryUserContext).`when`(context).createContextAsUser(SECONDARY_USER_HANDLE, 0)
+ doReturn(mPackageManagerSecondaryUser).`when`(secondaryUserContext).packageManager
+
+ for (app in listOf(SMS_APP1, SMS_APP2)) {
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .`when`(mPackageManagerPrimaryUser)
+ .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, app)
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .`when`(mPackageManagerSecondaryUser)
+ .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, app)
+ }
+
+ // Initialise message application primary user package1
+ val applicationInfo1 = ApplicationInfo()
+ applicationInfo1.uid = PRIMARY_USER_SMS_APP_UID1
+ doReturn(applicationInfo1)
+ .`when`(mPackageManagerPrimaryUser)
+ .getApplicationInfo(eq(SMS_APP1), anyInt())
+
+ // Initialise message application primary user package2
+ val applicationInfo2 = ApplicationInfo()
+ applicationInfo2.uid = PRIMARY_USER_SMS_APP_UID2
+ doReturn(applicationInfo2)
+ .`when`(mPackageManagerPrimaryUser)
+ .getApplicationInfo(eq(SMS_APP2), anyInt())
+
+ // Initialise message application secondary user package1
+ val applicationInfo3 = ApplicationInfo()
+ applicationInfo3.uid = SECONDARY_USER_SMS_APP_UID1
+ doReturn(applicationInfo3)
+ .`when`(mPackageManagerSecondaryUser)
+ .getApplicationInfo(eq(SMS_APP1), anyInt())
+
+ // Initialise message application secondary user package2
+ val applicationInfo4 = ApplicationInfo()
+ applicationInfo4.uid = SECONDARY_USER_SMS_APP_UID2
+ doReturn(applicationInfo4)
+ .`when`(mPackageManagerSecondaryUser)
+ .getApplicationInfo(eq(SMS_APP2), anyInt())
+ }
+
+ @Test
+ fun test_onRoleHoldersChanged_SatelliteFallbackUid_Changed_SingleUser() {
+ startSatelliteAccessController()
+ doReturn(listOf<String>()).`when`(mDeps).getRoleHoldersAsUser(
+ RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE
+ )
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
+
+ // check DEFAULT_MESSAGING_APP1 is available as satellite network fallback uid
+ doReturn(listOf(SMS_APP1))
+ .`when`(mDeps).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID1))
+
+ // check SMS_APP2 is available as satellite network Fallback uid
+ doReturn(listOf(SMS_APP2)).`when`(mDeps).getRoleHoldersAsUser(
+ RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE
+ )
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
+
+ // check no uid is available as satellite network fallback uid
+ doReturn(listOf<String>()).`when`(mDeps).getRoleHoldersAsUser(
+ RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE
+ )
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(ArraySet())
+ }
+
+ @Test
+ fun test_onRoleHoldersChanged_NoSatelliteCommunicationPermission() {
+ startSatelliteAccessController()
+ doReturn(listOf<Any>()).`when`(mDeps).getRoleHoldersAsUser(
+ RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE
+ )
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
+
+ // check DEFAULT_MESSAGING_APP1 is not available as satellite network fallback uid
+ // since satellite communication permission not available.
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .`when`(mPackageManagerPrimaryUser)
+ .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, SMS_APP1)
+ doReturn(listOf(SMS_APP1))
+ .`when`(mDeps).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
+ }
+
+ @Test
+ fun test_onRoleHoldersChanged_RoleSms_NotAvailable() {
+ startSatelliteAccessController()
+ doReturn(listOf(SMS_APP1))
+ .`when`(mDeps).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(
+ RoleManager.ROLE_BROWSER,
+ PRIMARY_USER_HANDLE
+ )
+ verify(mCallback, never()).accept(any())
+ }
+
+ @Test
+ fun test_onRoleHoldersChanged_SatelliteNetworkFallbackUid_Changed_multiUser() {
+ startSatelliteAccessController()
+ doReturn(listOf<String>()).`when`(mDeps).getRoleHoldersAsUser(
+ RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE
+ )
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback, never()).accept(any())
+
+ // check SMS_APP1 is available as satellite network fallback uid at primary user
+ doReturn(listOf(SMS_APP1))
+ .`when`(mDeps).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID1))
+
+ // check SMS_APP2 is available as satellite network fallback uid at primary user
+ doReturn(listOf(SMS_APP2)).`when`(mDeps).getRoleHoldersAsUser(
+ RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE
+ )
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
+
+ // check SMS_APP1 is available as satellite network fallback uid at secondary user
+ doReturn(listOf(SMS_APP1)).`when`(mDeps).getRoleHoldersAsUser(
+ RoleManager.ROLE_SMS,
+ SECONDARY_USER_HANDLE
+ )
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2, SECONDARY_USER_SMS_APP_UID1))
+
+ // check no uid is available as satellite network fallback uid at primary user
+ doReturn(listOf<String>()).`when`(mDeps).getRoleHoldersAsUser(
+ RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE
+ )
+ mRoleHolderChangedListener.onRoleHoldersChanged(
+ RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE
+ )
+ verify(mCallback).accept(setOf(SECONDARY_USER_SMS_APP_UID1))
+
+ // check SMS_APP2 is available as satellite network fallback uid at secondary user
+ doReturn(listOf(SMS_APP2))
+ .`when`(mDeps).getRoleHoldersAsUser(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(SECONDARY_USER_SMS_APP_UID2))
+
+ // check no uid is available as satellite network fallback uid at secondary user
+ doReturn(listOf<String>()).`when`(mDeps).getRoleHoldersAsUser(
+ RoleManager.ROLE_SMS,
+ SECONDARY_USER_HANDLE
+ )
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ verify(mCallback).accept(ArraySet())
+ }
+
+ @Test
+ fun test_SatelliteFallbackUidCallback_OnUserRemoval() {
+ startSatelliteAccessController()
+ // check SMS_APP2 is available as satellite network fallback uid at primary user
+ doReturn(listOf(SMS_APP2)).`when`(mDeps).getRoleHoldersAsUser(
+ RoleManager.ROLE_SMS,
+ PRIMARY_USER_HANDLE
+ )
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
+
+ // check SMS_APP1 is available as satellite network fallback uid at secondary user
+ doReturn(listOf(SMS_APP1)).`when`(mDeps).getRoleHoldersAsUser(
+ RoleManager.ROLE_SMS,
+ SECONDARY_USER_HANDLE
+ )
+ mRoleHolderChangedListener.onRoleHoldersChanged(RoleManager.ROLE_SMS, SECONDARY_USER_HANDLE)
+ verify(mCallback).accept(setOf(PRIMARY_USER_SMS_APP_UID2, SECONDARY_USER_SMS_APP_UID1))
+
+ val userRemovalIntent = Intent(Intent.ACTION_USER_REMOVED)
+ userRemovalIntent.putExtra(Intent.EXTRA_USER, SECONDARY_USER_HANDLE)
+ mUserRemovedReceiver.onReceive(context, userRemovalIntent)
+ verify(mCallback, times(2)).accept(setOf(PRIMARY_USER_SMS_APP_UID2))
+ }
+
+ @Test
+ fun testOnStartUpCallbackSatelliteFallbackUidWithExistingUsers() {
+ doReturn(
+ listOf(PRIMARY_USER_HANDLE)
+ ).`when`(userManager).getUserHandles(true)
+ doReturn(listOf(SMS_APP1))
+ .`when`(mDeps).getRoleHoldersAsUser(RoleManager.ROLE_SMS, PRIMARY_USER_HANDLE)
+ // At start up, SatelliteAccessController must call CS callback with existing users'
+ // default messaging apps uids.
+ startSatelliteAccessController()
+ verify(mCallback, timeout(500)).accept(setOf(PRIMARY_USER_SMS_APP_UID1))
+ }
+
+ private fun startSatelliteAccessController() {
+ mSatelliteAccessController.start()
+ // Get registered listener using captor
+ val listenerCaptor = ArgumentCaptor.forClass(OnRoleHoldersChangedListener::class.java)
+ verify(mDeps).addOnRoleHoldersChangedListenerAsUser(
+ any(Executor::class.java),
+ listenerCaptor.capture(),
+ any(UserHandle::class.java)
+ )
+ mRoleHolderChangedListener = listenerCaptor.value
+
+ // Get registered receiver using captor
+ val userRemovedReceiverCaptor = ArgumentCaptor.forClass(BroadcastReceiver::class.java)
+ verify(context).registerReceiver(
+ userRemovedReceiverCaptor.capture(),
+ any(IntentFilter::class.java),
+ isNull(),
+ any(Handler::class.java)
+ )
+ mUserRemovedReceiver = userRemovedReceiverCaptor.value
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
deleted file mode 100644
index c9cece0..0000000
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ /dev/null
@@ -1,3298 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.connectivity;
-
-import static android.Manifest.permission.BIND_VPN_SERVICE;
-import static android.Manifest.permission.CONTROL_VPN;
-import static android.content.pm.PackageManager.PERMISSION_DENIED;
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback;
-import static android.net.ConnectivityDiagnosticsManager.DataStallReport;
-import static android.net.ConnectivityManager.NetworkCallback;
-import static android.net.INetd.IF_STATE_DOWN;
-import static android.net.INetd.IF_STATE_UP;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
-import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
-import static android.net.RouteInfo.RTN_UNREACHABLE;
-import static android.net.VpnManager.TYPE_VPN_PLATFORM;
-import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.TEST_IDENTITY;
-import static android.net.cts.util.IkeSessionTestUtils.TEST_KEEPALIVE_TIMEOUT_UNSET;
-import static android.net.cts.util.IkeSessionTestUtils.getTestIkeSessionParams;
-import static android.net.ipsec.ike.IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_AUTO;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_NONE;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_UDP;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_AUTO;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_IPV4;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_IPV6;
-import static android.os.UserHandle.PER_USER_RANGE;
-import static android.telephony.CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL;
-import static android.telephony.CarrierConfigManager.KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT;
-import static android.telephony.CarrierConfigManager.KEY_PREFERRED_IKE_PROTOCOL_INT;
-
-import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
-import static com.android.server.connectivity.Vpn.AUTOMATIC_KEEPALIVE_DELAY_SECONDS;
-import static com.android.server.connectivity.Vpn.DEFAULT_LONG_LIVED_TCP_CONNS_EXPENSIVE_TIMEOUT_SEC;
-import static com.android.server.connectivity.Vpn.DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_AUTO;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV4_UDP;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_ESP;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_UDP;
-import static com.android.testutils.HandlerUtils.waitForIdleSerialExecutor;
-import static com.android.testutils.MiscAsserts.assertThrows;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.longThat;
-import static org.mockito.Mockito.after;
-import static org.mockito.Mockito.atLeast;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doCallRealMethod;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.annotation.NonNull;
-import android.annotation.UserIdInt;
-import android.app.AppOpsManager;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.content.pm.UserInfo;
-import android.content.res.Resources;
-import android.net.ConnectivityDiagnosticsManager;
-import android.net.ConnectivityManager;
-import android.net.INetd;
-import android.net.Ikev2VpnProfile;
-import android.net.InetAddresses;
-import android.net.InterfaceConfigurationParcel;
-import android.net.IpPrefix;
-import android.net.IpSecConfig;
-import android.net.IpSecManager;
-import android.net.IpSecTransform;
-import android.net.IpSecTunnelInterfaceResponse;
-import android.net.LinkAddress;
-import android.net.LinkProperties;
-import android.net.Network;
-import android.net.NetworkAgent;
-import android.net.NetworkAgentConfig;
-import android.net.NetworkCapabilities;
-import android.net.NetworkInfo.DetailedState;
-import android.net.RouteInfo;
-import android.net.TelephonyNetworkSpecifier;
-import android.net.UidRangeParcel;
-import android.net.VpnManager;
-import android.net.VpnProfileState;
-import android.net.VpnService;
-import android.net.VpnTransportInfo;
-import android.net.ipsec.ike.ChildSessionCallback;
-import android.net.ipsec.ike.ChildSessionConfiguration;
-import android.net.ipsec.ike.IkeFqdnIdentification;
-import android.net.ipsec.ike.IkeSessionCallback;
-import android.net.ipsec.ike.IkeSessionConfiguration;
-import android.net.ipsec.ike.IkeSessionConnectionInfo;
-import android.net.ipsec.ike.IkeSessionParams;
-import android.net.ipsec.ike.IkeTrafficSelector;
-import android.net.ipsec.ike.IkeTunnelConnectionParams;
-import android.net.ipsec.ike.exceptions.IkeException;
-import android.net.ipsec.ike.exceptions.IkeNetworkLostException;
-import android.net.ipsec.ike.exceptions.IkeNonProtocolException;
-import android.net.ipsec.ike.exceptions.IkeProtocolException;
-import android.net.ipsec.ike.exceptions.IkeTimeoutException;
-import android.net.vcn.VcnTransportInfo;
-import android.net.wifi.WifiInfo;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.os.INetworkManagementService;
-import android.os.ParcelFileDescriptor;
-import android.os.PersistableBundle;
-import android.os.PowerWhitelistManager;
-import android.os.Process;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.os.test.TestLooper;
-import android.provider.Settings;
-import android.security.Credentials;
-import android.telephony.CarrierConfigManager;
-import android.telephony.SubscriptionInfo;
-import android.telephony.SubscriptionManager;
-import android.telephony.TelephonyManager;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-import android.util.Pair;
-import android.util.Range;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.R;
-import com.android.internal.net.LegacyVpnInfo;
-import com.android.internal.net.VpnConfig;
-import com.android.internal.net.VpnProfile;
-import com.android.internal.util.HexDump;
-import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.DeviceIdleInternal;
-import com.android.server.IpSecService;
-import com.android.server.VpnTestBase;
-import com.android.server.vcn.util.PersistableBundleUtils;
-import com.android.testutils.DevSdkIgnoreRule;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.AdditionalAnswers;
-import org.mockito.Answers;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.net.Inet4Address;
-import java.net.Inet6Address;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Tests for {@link Vpn}.
- *
- * Build, install and run with:
- * runtest frameworks-net -c com.android.server.connectivity.VpnTest
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class VpnTest extends VpnTestBase {
- private static final String TAG = "VpnTest";
-
- @Rule
- public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
-
- static final Network EGRESS_NETWORK = new Network(101);
- static final String EGRESS_IFACE = "wlan0";
- private static final String TEST_VPN_CLIENT = "2.4.6.8";
- private static final String TEST_VPN_SERVER = "1.2.3.4";
- private static final String TEST_VPN_IDENTITY = "identity";
- private static final byte[] TEST_VPN_PSK = "psk".getBytes();
-
- private static final int IP4_PREFIX_LEN = 32;
- private static final int IP6_PREFIX_LEN = 64;
- private static final int MIN_PORT = 0;
- private static final int MAX_PORT = 65535;
-
- private static final InetAddress TEST_VPN_CLIENT_IP =
- InetAddresses.parseNumericAddress(TEST_VPN_CLIENT);
- private static final InetAddress TEST_VPN_SERVER_IP =
- InetAddresses.parseNumericAddress(TEST_VPN_SERVER);
- private static final InetAddress TEST_VPN_CLIENT_IP_2 =
- InetAddresses.parseNumericAddress("192.0.2.200");
- private static final InetAddress TEST_VPN_SERVER_IP_2 =
- InetAddresses.parseNumericAddress("192.0.2.201");
- private static final InetAddress TEST_VPN_INTERNAL_IP =
- InetAddresses.parseNumericAddress("198.51.100.10");
- private static final InetAddress TEST_VPN_INTERNAL_IP6 =
- InetAddresses.parseNumericAddress("2001:db8::1");
- private static final InetAddress TEST_VPN_INTERNAL_DNS =
- InetAddresses.parseNumericAddress("8.8.8.8");
- private static final InetAddress TEST_VPN_INTERNAL_DNS6 =
- InetAddresses.parseNumericAddress("2001:4860:4860::8888");
-
- private static final IkeTrafficSelector IN_TS =
- new IkeTrafficSelector(MIN_PORT, MAX_PORT, TEST_VPN_INTERNAL_IP, TEST_VPN_INTERNAL_IP);
- private static final IkeTrafficSelector IN_TS6 =
- new IkeTrafficSelector(
- MIN_PORT, MAX_PORT, TEST_VPN_INTERNAL_IP6, TEST_VPN_INTERNAL_IP6);
- private static final IkeTrafficSelector OUT_TS =
- new IkeTrafficSelector(MIN_PORT, MAX_PORT,
- InetAddresses.parseNumericAddress("0.0.0.0"),
- InetAddresses.parseNumericAddress("255.255.255.255"));
- private static final IkeTrafficSelector OUT_TS6 =
- new IkeTrafficSelector(
- MIN_PORT,
- MAX_PORT,
- InetAddresses.parseNumericAddress("::"),
- InetAddresses.parseNumericAddress("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
-
- private static final Network TEST_NETWORK = new Network(Integer.MAX_VALUE);
- private static final Network TEST_NETWORK_2 = new Network(Integer.MAX_VALUE - 1);
- private static final String TEST_IFACE_NAME = "TEST_IFACE";
- private static final int TEST_TUNNEL_RESOURCE_ID = 0x2345;
- private static final long TEST_TIMEOUT_MS = 500L;
- private static final long TIMEOUT_CROSSTHREAD_MS = 20_000L;
- private static final String PRIMARY_USER_APP_EXCLUDE_KEY =
- "VPNAPPEXCLUDED_27_com.testvpn.vpn";
- static final String PKGS_BYTES = getPackageByteString(List.of(PKGS));
- private static final Range<Integer> PRIMARY_USER_RANGE = uidRangeForUser(PRIMARY_USER.id);
- private static final int TEST_KEEPALIVE_TIMER = 800;
- private static final int TEST_SUB_ID = 1234;
- private static final String TEST_MCCMNC = "12345";
-
- @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext;
- @Mock private UserManager mUserManager;
- @Mock private PackageManager mPackageManager;
- @Mock private INetworkManagementService mNetService;
- @Mock private INetd mNetd;
- @Mock private AppOpsManager mAppOps;
- @Mock private NotificationManager mNotificationManager;
- @Mock private Vpn.SystemServices mSystemServices;
- @Mock private Vpn.IkeSessionWrapper mIkeSessionWrapper;
- @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator;
- @Mock private Vpn.VpnNetworkAgentWrapper mMockNetworkAgent;
- @Mock private ConnectivityManager mConnectivityManager;
- @Mock private ConnectivityDiagnosticsManager mCdm;
- @Mock private TelephonyManager mTelephonyManager;
- @Mock private TelephonyManager mTmPerSub;
- @Mock private CarrierConfigManager mConfigManager;
- @Mock private SubscriptionManager mSubscriptionManager;
- @Mock private IpSecService mIpSecService;
- @Mock private VpnProfileStore mVpnProfileStore;
- private final TestExecutor mExecutor;
- @Mock DeviceIdleInternal mDeviceIdleInternal;
- private final VpnProfile mVpnProfile;
-
- @Captor private ArgumentCaptor<Collection<Range<Integer>>> mUidRangesCaptor;
-
- private IpSecManager mIpSecManager;
- private TestDeps mTestDeps;
-
- public static class TestExecutor extends ScheduledThreadPoolExecutor {
- public static final long REAL_DELAY = -1;
-
- // For the purposes of the test, run all scheduled tasks after 10ms to save
- // execution time, unless overridden by the specific test. Set to REAL_DELAY
- // to actually wait for the delay specified by the real call to schedule().
- public long delayMs = 10;
- // If this is true, execute() will call the runnable inline. This is useful because
- // super.execute() calls schedule(), which messes with checks that scheduled() is
- // called a given number of times.
- public boolean executeDirect = false;
-
- public TestExecutor() {
- super(1);
- }
-
- @Override
- public void execute(final Runnable command) {
- // See |executeDirect| for why this is necessary.
- if (executeDirect) {
- command.run();
- } else {
- super.execute(command);
- }
- }
-
- @Override
- public ScheduledFuture<?> schedule(final Runnable command, final long delay,
- TimeUnit unit) {
- if (0 == delay || delayMs == REAL_DELAY) {
- // super.execute() calls schedule() with 0, so use the real delay if it's 0.
- return super.schedule(command, delay, unit);
- } else {
- return super.schedule(command, delayMs, TimeUnit.MILLISECONDS);
- }
- }
- }
-
- public VpnTest() throws Exception {
- // Build an actual VPN profile that is capable of being converted to and from an
- // Ikev2VpnProfile
- final Ikev2VpnProfile.Builder builder =
- new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY);
- builder.setAuthPsk(TEST_VPN_PSK);
- builder.setBypassable(true /* isBypassable */);
- mExecutor = spy(new TestExecutor());
- mVpnProfile = builder.build().toVpnProfile();
- }
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
-
- mIpSecManager = new IpSecManager(mContext, mIpSecService);
- mTestDeps = spy(new TestDeps());
- doReturn(IPV6_MIN_MTU)
- .when(mTestDeps)
- .calculateVpnMtu(any(), anyInt(), anyInt(), anyBoolean());
- doReturn(1500).when(mTestDeps).getJavaNetworkInterfaceMtu(any(), anyInt());
-
- when(mContext.getPackageManager()).thenReturn(mPackageManager);
- setMockedPackages(sPackages);
-
- when(mContext.getPackageName()).thenReturn(TEST_VPN_PKG);
- when(mContext.getOpPackageName()).thenReturn(TEST_VPN_PKG);
- mockService(UserManager.class, Context.USER_SERVICE, mUserManager);
- mockService(AppOpsManager.class, Context.APP_OPS_SERVICE, mAppOps);
- mockService(NotificationManager.class, Context.NOTIFICATION_SERVICE, mNotificationManager);
- mockService(ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mConnectivityManager);
- mockService(IpSecManager.class, Context.IPSEC_SERVICE, mIpSecManager);
- mockService(ConnectivityDiagnosticsManager.class, Context.CONNECTIVITY_DIAGNOSTICS_SERVICE,
- mCdm);
- mockService(TelephonyManager.class, Context.TELEPHONY_SERVICE, mTelephonyManager);
- mockService(CarrierConfigManager.class, Context.CARRIER_CONFIG_SERVICE, mConfigManager);
- mockService(SubscriptionManager.class, Context.TELEPHONY_SUBSCRIPTION_SERVICE,
- mSubscriptionManager);
- doReturn(mTmPerSub).when(mTelephonyManager).createForSubscriptionId(anyInt());
- when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent))
- .thenReturn(Resources.getSystem().getString(
- R.string.config_customVpnAlwaysOnDisconnectedDialogComponent));
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS))
- .thenReturn(true);
-
- // Used by {@link Notification.Builder}
- ApplicationInfo applicationInfo = new ApplicationInfo();
- applicationInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT;
- when(mContext.getApplicationInfo()).thenReturn(applicationInfo);
- when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
- .thenReturn(applicationInfo);
-
- doNothing().when(mNetService).registerObserver(any());
-
- // Deny all appops by default.
- when(mAppOps.noteOpNoThrow(anyString(), anyInt(), anyString(), any(), any()))
- .thenReturn(AppOpsManager.MODE_IGNORED);
-
- // Setup IpSecService
- final IpSecTunnelInterfaceResponse tunnelResp =
- new IpSecTunnelInterfaceResponse(
- IpSecManager.Status.OK, TEST_TUNNEL_RESOURCE_ID, TEST_IFACE_NAME);
- when(mIpSecService.createTunnelInterface(any(), any(), any(), any(), any()))
- .thenReturn(tunnelResp);
- doReturn(new LinkProperties()).when(mConnectivityManager).getLinkProperties(any());
-
- // The unit test should know what kind of permission it needs and set the permission by
- // itself, so set the default value of Context#checkCallingOrSelfPermission to
- // PERMISSION_DENIED.
- doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(any());
-
- // Set up mIkev2SessionCreator and mExecutor
- resetIkev2SessionCreator(mIkeSessionWrapper);
- }
-
- private void resetIkev2SessionCreator(Vpn.IkeSessionWrapper ikeSession) {
- reset(mIkev2SessionCreator);
- when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
- .thenReturn(ikeSession);
- }
-
- private <T> void mockService(Class<T> clazz, String name, T service) {
- doReturn(service).when(mContext).getSystemService(name);
- doReturn(name).when(mContext).getSystemServiceName(clazz);
- if (mContext.getSystemService(clazz).getClass().equals(Object.class)) {
- // Test is using mockito-extended (mContext uses Answers.RETURNS_DEEP_STUBS and returned
- // a mock object on a final method)
- doCallRealMethod().when(mContext).getSystemService(clazz);
- }
- }
-
- private Set<Range<Integer>> rangeSet(Range<Integer> ... ranges) {
- final Set<Range<Integer>> range = new ArraySet<>();
- for (Range<Integer> r : ranges) range.add(r);
-
- return range;
- }
-
- private static Range<Integer> uidRangeForUser(int userId) {
- return new Range<Integer>(userId * PER_USER_RANGE, (userId + 1) * PER_USER_RANGE - 1);
- }
-
- private Range<Integer> uidRange(int start, int stop) {
- return new Range<Integer>(start, stop);
- }
-
- private static String getPackageByteString(List<String> packages) {
- try {
- return HexDump.toHexString(
- PersistableBundleUtils.toDiskStableBytes(PersistableBundleUtils.fromList(
- packages, PersistableBundleUtils.STRING_SERIALIZER)),
- true /* upperCase */);
- } catch (IOException e) {
- return null;
- }
- }
-
- @Test
- public void testRestrictedProfilesAreAddedToVpn() {
- setMockedUsers(PRIMARY_USER, SECONDARY_USER, RESTRICTED_PROFILE_A, RESTRICTED_PROFILE_B);
-
- final Vpn vpn = createVpn(PRIMARY_USER.id);
-
- // Assume the user can have restricted profiles.
- doReturn(true).when(mUserManager).canHaveRestrictedProfile();
- final Set<Range<Integer>> ranges =
- vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id, null, null);
-
- assertEquals(rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id)),
- ranges);
- }
-
- @Test
- public void testManagedProfilesAreNotAddedToVpn() {
- setMockedUsers(PRIMARY_USER, MANAGED_PROFILE_A);
-
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final Set<Range<Integer>> ranges = vpn.createUserAndRestrictedProfilesRanges(
- PRIMARY_USER.id, null, null);
-
- assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
- }
-
- @Test
- public void testAddUserToVpnOnlyAddsOneUser() {
- setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A, MANAGED_PROFILE_A);
-
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final Set<Range<Integer>> ranges = new ArraySet<>();
- vpn.addUserToRanges(ranges, PRIMARY_USER.id, null, null);
-
- assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
- }
-
- @Test
- public void testUidAllowAndDenylist() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final Range<Integer> user = PRIMARY_USER_RANGE;
- final int userStart = user.getLower();
- final int userStop = user.getUpper();
- final String[] packages = {PKGS[0], PKGS[1], PKGS[2]};
-
- // Allowed list
- final Set<Range<Integer>> allow = vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
- Arrays.asList(packages), null /* disallowedApplications */);
- assertEquals(rangeSet(
- uidRange(userStart + PKG_UIDS[0], userStart + PKG_UIDS[0]),
- uidRange(userStart + PKG_UIDS[1], userStart + PKG_UIDS[2]),
- uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0]),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0])),
- uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[1]),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[2]))),
- allow);
-
- // Denied list
- final Set<Range<Integer>> disallow =
- vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
- null /* allowedApplications */, Arrays.asList(packages));
- assertEquals(rangeSet(
- uidRange(userStart, userStart + PKG_UIDS[0] - 1),
- uidRange(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
- /* Empty range between UIDS[1] and UIDS[2], should be excluded, */
- uidRange(userStart + PKG_UIDS[2] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)),
- disallow);
- }
-
- private void verifyPowerSaveTempWhitelistApp(String packageName) {
- verify(mDeviceIdleInternal, timeout(TEST_TIMEOUT_MS)).addPowerSaveTempWhitelistApp(
- anyInt(), eq(packageName), anyLong(), anyInt(), eq(false),
- eq(PowerWhitelistManager.REASON_VPN), eq("VpnManager event"));
- }
-
- @Test
- public void testGetAlwaysAndOnGetLockDown() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
-
- // Default state.
- assertFalse(vpn.getAlwaysOn());
- assertFalse(vpn.getLockdown());
-
- // Set always-on without lockdown.
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, Collections.emptyList()));
- assertTrue(vpn.getAlwaysOn());
- assertFalse(vpn.getLockdown());
-
- // Set always-on with lockdown.
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, Collections.emptyList()));
- assertTrue(vpn.getAlwaysOn());
- assertTrue(vpn.getLockdown());
-
- // Remove always-on configuration.
- assertTrue(vpn.setAlwaysOnPackage(null, false, Collections.emptyList()));
- assertFalse(vpn.getAlwaysOn());
- assertFalse(vpn.getLockdown());
- }
-
- @Test
- public void testAlwaysOnWithoutLockdown() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[1], false /* lockdown */, null /* lockdownAllowlist */));
- verify(mConnectivityManager, never()).setRequireVpnForUids(anyBoolean(), any());
-
- assertTrue(vpn.setAlwaysOnPackage(
- null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
- verify(mConnectivityManager, never()).setRequireVpnForUids(anyBoolean(), any());
- }
-
- @Test
- public void testLockdownChangingPackage() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final Range<Integer> user = PRIMARY_USER_RANGE;
- final int userStart = user.getLower();
- final int userStop = user.getUpper();
- // Set always-on without lockdown.
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, null));
-
- // Set always-on with lockdown.
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
- }));
-
- // Switch to another app.
- assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null));
- verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
- }));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart, userStart + PKG_UIDS[3] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
- }));
- }
-
- @Test
- public void testLockdownAllowlist() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final Range<Integer> user = PRIMARY_USER_RANGE;
- final int userStart = user.getLower();
- final int userStop = user.getUpper();
- // Set always-on with lockdown and allow app PKGS[2] from lockdown.
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[1], true, Collections.singletonList(PKGS[2])));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1]) - 1),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
- }));
- // Change allowed app list to PKGS[3].
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[1], true, Collections.singletonList(PKGS[3])));
- verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
- }));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
- }));
-
- // Change the VPN app.
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[0], true, Collections.singletonList(PKGS[3])));
- verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
- }));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart, userStart + PKG_UIDS[0] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
- }));
-
- // Remove the list of allowed packages.
- assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null));
- verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
- }));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
- }));
-
- // Add the list of allowed packages.
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[0], true, Collections.singletonList(PKGS[1])));
- verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
- }));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
- }));
-
- // Try allowing a package with a comma, should be rejected.
- assertFalse(vpn.setAlwaysOnPackage(
- PKGS[0], true, Collections.singletonList("a.b,c.d")));
-
- // Pass a non-existent packages in the allowlist, they (and only they) should be ignored.
- // allowed package should change from PGKS[1] to PKGS[2].
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[0], true, Arrays.asList("com.foo.app", PKGS[2], "com.bar.app")));
- verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
- }));
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[2] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
- Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
- Process.toSdkSandboxUid(userStart + PKG_UIDS[2] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
- }));
- }
-
- @Test
- public void testLockdownSystemUser() throws Exception {
- final Vpn vpn = createVpn(SYSTEM_USER_ID);
-
- // Uid 0 is always excluded and PKG_UIDS[1] is the uid of the VPN.
- final List<Integer> excludedUids = new ArrayList<>(List.of(0, PKG_UIDS[1]));
- final List<Range<Integer>> ranges = makeVpnUidRange(SYSTEM_USER_ID, excludedUids);
-
- // Set always-on with lockdown.
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[1], true /* lockdown */, null /* lockdownAllowlist */));
- verify(mConnectivityManager).setRequireVpnForUids(true, ranges);
-
- // Disable always-on with lockdown.
- assertTrue(vpn.setAlwaysOnPackage(
- null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
- verify(mConnectivityManager).setRequireVpnForUids(false, ranges);
-
- // Set always-on with lockdown and allow the app PKGS[2].
- excludedUids.add(PKG_UIDS[2]);
- final List<Range<Integer>> ranges2 = makeVpnUidRange(SYSTEM_USER_ID, excludedUids);
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[1], true /* lockdown */, Collections.singletonList(PKGS[2])));
- verify(mConnectivityManager).setRequireVpnForUids(true, ranges2);
-
- // Disable always-on with lockdown.
- assertTrue(vpn.setAlwaysOnPackage(
- null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
- verify(mConnectivityManager).setRequireVpnForUids(false, ranges2);
- }
-
- @Test
- public void testLockdownRuleRepeatability() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
- new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())};
- // Given legacy lockdown is already enabled,
- vpn.setLockdown(true);
- verify(mConnectivityManager, times(1)).setRequireVpnForUids(true,
- toRanges(primaryUserRangeParcel));
-
- // Enabling legacy lockdown twice should do nothing.
- vpn.setLockdown(true);
- verify(mConnectivityManager, times(1)).setRequireVpnForUids(anyBoolean(), any());
-
- // And disabling should remove the rules exactly once.
- vpn.setLockdown(false);
- verify(mConnectivityManager, times(1)).setRequireVpnForUids(false,
- toRanges(primaryUserRangeParcel));
-
- // Removing the lockdown again should have no effect.
- vpn.setLockdown(false);
- verify(mConnectivityManager, times(2)).setRequireVpnForUids(anyBoolean(), any());
- }
-
- private ArrayList<Range<Integer>> toRanges(UidRangeParcel[] ranges) {
- ArrayList<Range<Integer>> rangesArray = new ArrayList<>(ranges.length);
- for (int i = 0; i < ranges.length; i++) {
- rangesArray.add(new Range<>(ranges[i].start, ranges[i].stop));
- }
- return rangesArray;
- }
-
- @Test
- public void testLockdownRuleReversibility() throws Exception {
- doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final UidRangeParcel[] entireUser = {
- new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())
- };
- final UidRangeParcel[] exceptPkg0 = {
- new UidRangeParcel(entireUser[0].start, entireUser[0].start + PKG_UIDS[0] - 1),
- new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1,
- Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] - 1)),
- new UidRangeParcel(Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] + 1),
- entireUser[0].stop),
- };
-
- final InOrder order = inOrder(mConnectivityManager);
-
- // Given lockdown is enabled with no package (legacy VPN),
- vpn.setLockdown(true);
- order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser));
-
- // When a new VPN package is set the rules should change to cover that package.
- vpn.prepare(null, PKGS[0], VpnManager.TYPE_VPN_SERVICE);
- order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(entireUser));
- order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(exceptPkg0));
-
- // When that VPN package is unset, everything should be undone again in reverse.
- vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE);
- order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(exceptPkg0));
- order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser));
- }
-
- @Test
- public void testOnUserAddedAndRemoved_restrictedUser() throws Exception {
- final InOrder order = inOrder(mMockNetworkAgent);
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final Set<Range<Integer>> initialRange = rangeSet(PRIMARY_USER_RANGE);
- // Note since mVpnProfile is a Ikev2VpnProfile, this starts an IkeV2VpnRunner.
- startLegacyVpn(vpn, mVpnProfile);
- // Set an initial Uid range and mock the network agent
- vpn.mNetworkCapabilities.setUids(initialRange);
- vpn.mNetworkAgent = mMockNetworkAgent;
-
- // Add the restricted user
- setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
- vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
- // Expect restricted user range to be added to the NetworkCapabilities.
- final Set<Range<Integer>> expectRestrictedRange =
- rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id));
- assertEquals(expectRestrictedRange, vpn.mNetworkCapabilities.getUids());
- order.verify(mMockNetworkAgent).doSendNetworkCapabilities(
- argThat(nc -> expectRestrictedRange.equals(nc.getUids())));
-
- // Remove the restricted user
- vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
- // Expect restricted user range to be removed from the NetworkCapabilities.
- assertEquals(initialRange, vpn.mNetworkCapabilities.getUids());
- order.verify(mMockNetworkAgent).doSendNetworkCapabilities(
- argThat(nc -> initialRange.equals(nc.getUids())));
- }
-
- @Test
- public void testOnUserAddedAndRemoved_restrictedUserLockdown() throws Exception {
- final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
- new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())};
- final Range<Integer> restrictedUserRange = uidRangeForUser(RESTRICTED_PROFILE_A.id);
- final UidRangeParcel[] restrictedUserRangeParcel = new UidRangeParcel[] {
- new UidRangeParcel(restrictedUserRange.getLower(), restrictedUserRange.getUpper())};
- final Vpn vpn = createVpn(PRIMARY_USER.id);
-
- // Set lockdown calls setRequireVpnForUids
- vpn.setLockdown(true);
- verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(primaryUserRangeParcel));
-
- // Add the restricted user
- doReturn(true).when(mUserManager).canHaveRestrictedProfile();
- setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
- vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
-
- // Expect restricted user range to be added.
- verify(mConnectivityManager).setRequireVpnForUids(true,
- toRanges(restrictedUserRangeParcel));
-
- // Mark as partial indicates that the user is removed, mUserManager.getAliveUsers() does not
- // return the restricted user but it is still returned in mUserManager.getUserInfo().
- RESTRICTED_PROFILE_A.partial = true;
- // Remove the restricted user
- vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
- verify(mConnectivityManager).setRequireVpnForUids(false,
- toRanges(restrictedUserRangeParcel));
- // reset to avoid affecting other tests since RESTRICTED_PROFILE_A is static.
- RESTRICTED_PROFILE_A.partial = false;
- }
-
- @Test
- public void testOnUserAddedAndRemoved_restrictedUserAlwaysOn() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
-
- // setAlwaysOnPackage() calls setRequireVpnForUids()
- assertTrue(vpn.setAlwaysOnPackage(
- PKGS[0], true /* lockdown */, null /* lockdownAllowlist */));
- final List<Integer> excludedUids = List.of(PKG_UIDS[0]);
- final List<Range<Integer>> primaryRanges =
- makeVpnUidRange(PRIMARY_USER.id, excludedUids);
- verify(mConnectivityManager).setRequireVpnForUids(true, primaryRanges);
-
- // Add the restricted user
- doReturn(true).when(mUserManager).canHaveRestrictedProfile();
- setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
- vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
-
- final List<Range<Integer>> restrictedRanges =
- makeVpnUidRange(RESTRICTED_PROFILE_A.id, excludedUids);
- // Expect restricted user range to be added.
- verify(mConnectivityManager).setRequireVpnForUids(true, restrictedRanges);
-
- // Mark as partial indicates that the user is removed, mUserManager.getAliveUsers() does not
- // return the restricted user but it is still returned in mUserManager.getUserInfo().
- RESTRICTED_PROFILE_A.partial = true;
- // Remove the restricted user
- vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
- verify(mConnectivityManager).setRequireVpnForUids(false, restrictedRanges);
-
- // reset to avoid affecting other tests since RESTRICTED_PROFILE_A is static.
- RESTRICTED_PROFILE_A.partial = false;
- }
-
- @Test
- public void testPrepare_throwSecurityExceptionWhenGivenPackageDoesNotBelongToTheCaller()
- throws Exception {
- mTestDeps.mIgnoreCallingUidChecks = false;
- final Vpn vpn = createVpn();
- assertThrows(SecurityException.class,
- () -> vpn.prepare("com.not.vpn.owner", null, VpnManager.TYPE_VPN_SERVICE));
- assertThrows(SecurityException.class,
- () -> vpn.prepare(null, "com.not.vpn.owner", VpnManager.TYPE_VPN_SERVICE));
- assertThrows(SecurityException.class,
- () -> vpn.prepare("com.not.vpn.owner1", "com.not.vpn.owner2",
- VpnManager.TYPE_VPN_SERVICE));
- }
-
- @Test
- public void testPrepare_bothOldPackageAndNewPackageAreNull() throws Exception {
- final Vpn vpn = createVpn();
- assertTrue(vpn.prepare(null, null, VpnManager.TYPE_VPN_SERVICE));
-
- }
-
- @Test
- public void testPrepare_legacyVpnWithoutControlVpn()
- throws Exception {
- doThrow(new SecurityException("no CONTROL_VPN")).when(mContext)
- .enforceCallingOrSelfPermission(eq(CONTROL_VPN), any());
- final Vpn vpn = createVpn();
- assertThrows(SecurityException.class,
- () -> vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE));
-
- // CONTROL_VPN can be held by the caller or another system server process - both are
- // allowed. Just checking for `enforceCallingPermission` may not be sufficient.
- verify(mContext, never()).enforceCallingPermission(eq(CONTROL_VPN), any());
- }
-
- @Test
- public void testPrepare_legacyVpnWithControlVpn()
- throws Exception {
- doNothing().when(mContext).enforceCallingOrSelfPermission(eq(CONTROL_VPN), any());
- final Vpn vpn = createVpn();
- assertTrue(vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE));
-
- // CONTROL_VPN can be held by the caller or another system server process - both are
- // allowed. Just checking for `enforceCallingPermission` may not be sufficient.
- verify(mContext, never()).enforceCallingPermission(eq(CONTROL_VPN), any());
- }
-
- @Test
- public void testIsAlwaysOnPackageSupported() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
-
- ApplicationInfo appInfo = new ApplicationInfo();
- when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(PRIMARY_USER.id)))
- .thenReturn(appInfo);
-
- ServiceInfo svcInfo = new ServiceInfo();
- ResolveInfo resInfo = new ResolveInfo();
- resInfo.serviceInfo = svcInfo;
- when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA),
- eq(PRIMARY_USER.id)))
- .thenReturn(Collections.singletonList(resInfo));
-
- // null package name should return false
- assertFalse(vpn.isAlwaysOnPackageSupported(null));
-
- // Pre-N apps are not supported
- appInfo.targetSdkVersion = VERSION_CODES.M;
- assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0]));
-
- // N+ apps are supported by default
- appInfo.targetSdkVersion = VERSION_CODES.N;
- assertTrue(vpn.isAlwaysOnPackageSupported(PKGS[0]));
-
- // Apps that opt out explicitly are not supported
- appInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT;
- Bundle metaData = new Bundle();
- metaData.putBoolean(VpnService.SERVICE_META_DATA_SUPPORTS_ALWAYS_ON, false);
- svcInfo.metaData = metaData;
- assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0]));
- }
-
- @Test
- public void testNotificationShownForAlwaysOnApp() throws Exception {
- final UserHandle userHandle = UserHandle.of(PRIMARY_USER.id);
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- setMockedUsers(PRIMARY_USER);
-
- final InOrder order = inOrder(mNotificationManager);
-
- // Don't show a notification for regular disconnected states.
- vpn.updateState(DetailedState.DISCONNECTED, TAG);
- order.verify(mNotificationManager, atLeastOnce()).cancel(anyString(), anyInt());
-
- // Start showing a notification for disconnected once always-on.
- vpn.setAlwaysOnPackage(PKGS[0], false, null);
- order.verify(mNotificationManager).notify(anyString(), anyInt(), any());
-
- // Stop showing the notification once connected.
- vpn.updateState(DetailedState.CONNECTED, TAG);
- order.verify(mNotificationManager).cancel(anyString(), anyInt());
-
- // Show the notification if we disconnect again.
- vpn.updateState(DetailedState.DISCONNECTED, TAG);
- order.verify(mNotificationManager).notify(anyString(), anyInt(), any());
-
- // Notification should be cleared after unsetting always-on package.
- vpn.setAlwaysOnPackage(null, false, null);
- order.verify(mNotificationManager).cancel(anyString(), anyInt());
- }
-
- /**
- * The profile name should NOT change between releases for backwards compatibility
- *
- * <p>If this is changed between releases, the {@link Vpn#getVpnProfilePrivileged()} method MUST
- * be updated to ensure backward compatibility.
- */
- @Test
- public void testGetProfileNameForPackage() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- setMockedUsers(PRIMARY_USER);
-
- final String expected = Credentials.PLATFORM_VPN + PRIMARY_USER.id + "_" + TEST_VPN_PKG;
- assertEquals(expected, vpn.getProfileNameForPackage(TEST_VPN_PKG));
- }
-
- private Vpn createVpn(String... grantedOps) throws Exception {
- return createVpn(PRIMARY_USER, grantedOps);
- }
-
- private Vpn createVpn(UserInfo user, String... grantedOps) throws Exception {
- final Vpn vpn = createVpn(user.id);
- setMockedUsers(user);
-
- for (final String opStr : grantedOps) {
- when(mAppOps.noteOpNoThrow(opStr, Process.myUid(), TEST_VPN_PKG,
- null /* attributionTag */, null /* message */))
- .thenReturn(AppOpsManager.MODE_ALLOWED);
- }
-
- return vpn;
- }
-
- private void checkProvisionVpnProfile(Vpn vpn, boolean expectedResult, String... checkedOps) {
- assertEquals(expectedResult, vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile));
-
- // The profile should always be stored, whether or not consent has been previously granted.
- verify(mVpnProfileStore)
- .put(
- eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)),
- eq(mVpnProfile.encode()));
-
- for (final String checkedOpStr : checkedOps) {
- verify(mAppOps).noteOpNoThrow(checkedOpStr, Process.myUid(), TEST_VPN_PKG,
- null /* attributionTag */, null /* message */);
- }
- }
-
- @Test
- public void testProvisionVpnProfileNoIpsecTunnels() throws Exception {
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS))
- .thenReturn(false);
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- try {
- checkProvisionVpnProfile(
- vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- fail("Expected exception due to missing feature");
- } catch (UnsupportedOperationException expected) {
- }
- }
-
- private String startVpnForVerifyAppExclusionList(Vpn vpn) throws Exception {
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
- when(mVpnProfileStore.get(PRIMARY_USER_APP_EXCLUDE_KEY))
- .thenReturn(HexDump.hexStringToByteArray(PKGS_BYTES));
- final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
- final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
- PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
- verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
- clearInvocations(mConnectivityManager);
- verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
- vpn.mNetworkAgent = mMockNetworkAgent;
-
- return sessionKey;
- }
-
- private Vpn prepareVpnForVerifyAppExclusionList() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- startVpnForVerifyAppExclusionList(vpn);
-
- return vpn;
- }
-
- @Test
- public void testSetAndGetAppExclusionList() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- final String sessionKey = startVpnForVerifyAppExclusionList(vpn);
- verify(mVpnProfileStore, never()).put(eq(PRIMARY_USER_APP_EXCLUDE_KEY), any());
- vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
- verify(mVpnProfileStore)
- .put(eq(PRIMARY_USER_APP_EXCLUDE_KEY),
- eq(HexDump.hexStringToByteArray(PKGS_BYTES)));
- final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
- PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
- verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
- assertEquals(uidRanges, vpn.mNetworkCapabilities.getUids());
- assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
- }
-
- @Test
- public void testRefreshPlatformVpnAppExclusionList_updatesExcludedUids() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- final String sessionKey = startVpnForVerifyAppExclusionList(vpn);
- vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
- final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
- PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
- verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
- verify(mMockNetworkAgent).doSendNetworkCapabilities(any());
- assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
-
- reset(mMockNetworkAgent);
-
- // Remove one of the package
- List<Integer> newExcludedUids = toList(PKG_UIDS);
- newExcludedUids.remove((Integer) PKG_UIDS[0]);
- Set<Range<Integer>> newUidRanges = makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids);
- sPackages.remove(PKGS[0]);
- vpn.refreshPlatformVpnAppExclusionList();
-
- // List in keystore is not changed, but UID for the removed packages is no longer exempted.
- assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
- assertEquals(newUidRanges, vpn.mNetworkCapabilities.getUids());
- ArgumentCaptor<NetworkCapabilities> ncCaptor =
- ArgumentCaptor.forClass(NetworkCapabilities.class);
- verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
- assertEquals(newUidRanges, ncCaptor.getValue().getUids());
- verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(newUidRanges));
-
- reset(mMockNetworkAgent);
-
- // Add the package back
- newExcludedUids.add(PKG_UIDS[0]);
- newUidRanges = makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids);
- sPackages.put(PKGS[0], PKG_UIDS[0]);
- vpn.refreshPlatformVpnAppExclusionList();
-
- // List in keystore is not changed and the uid list should be updated in the net cap.
- assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
- assertEquals(newUidRanges, vpn.mNetworkCapabilities.getUids());
- verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
- assertEquals(newUidRanges, ncCaptor.getValue().getUids());
-
- // The uidRange is the same as the original setAppExclusionList so this is the second call
- verify(mConnectivityManager, times(2))
- .setVpnDefaultForUids(eq(sessionKey), eq(newUidRanges));
- }
-
- private List<Range<Integer>> makeVpnUidRange(int userId, List<Integer> excludedAppIdList) {
- final SortedSet<Integer> list = new TreeSet<>();
-
- final int userBase = userId * UserHandle.PER_USER_RANGE;
- for (int appId : excludedAppIdList) {
- final int uid = UserHandle.getUid(userId, appId);
- list.add(uid);
- if (Process.isApplicationUid(uid)) {
- list.add(Process.toSdkSandboxUid(uid)); // Add Sdk Sandbox UID
- }
- }
-
- final int minUid = userBase;
- final int maxUid = userBase + UserHandle.PER_USER_RANGE - 1;
- final List<Range<Integer>> ranges = new ArrayList<>();
-
- // Iterate the list to create the ranges between each uid.
- int start = minUid;
- for (int uid : list) {
- if (uid == start) {
- start++;
- } else {
- ranges.add(new Range<>(start, uid - 1));
- start = uid + 1;
- }
- }
-
- // Create the range between last uid and max uid.
- if (start <= maxUid) {
- ranges.add(new Range<>(start, maxUid));
- }
-
- return ranges;
- }
-
- private Set<Range<Integer>> makeVpnUidRangeSet(int userId, List<Integer> excludedAppIdList) {
- return new ArraySet<>(makeVpnUidRange(userId, excludedAppIdList));
- }
-
- @Test
- public void testSetAndGetAppExclusionListRestrictedUser() throws Exception {
- final Vpn vpn = prepareVpnForVerifyAppExclusionList();
-
- // Mock it to restricted profile
- when(mUserManager.getUserInfo(anyInt())).thenReturn(RESTRICTED_PROFILE_A);
-
- // Restricted users cannot configure VPNs
- assertThrows(SecurityException.class,
- () -> vpn.setAppExclusionList(TEST_VPN_PKG, new ArrayList<>()));
-
- assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
- }
-
- @Test
- public void testProvisionVpnProfilePreconsented() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- checkProvisionVpnProfile(
- vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- }
-
- @Test
- public void testProvisionVpnProfileNotPreconsented() throws Exception {
- final Vpn vpn = createVpn();
-
- // Expect that both the ACTIVATE_VPN and ACTIVATE_PLATFORM_VPN were tried, but the caller
- // had neither.
- checkProvisionVpnProfile(vpn, false /* expectedResult */,
- AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN, AppOpsManager.OPSTR_ACTIVATE_VPN);
- }
-
- @Test
- public void testProvisionVpnProfileVpnServicePreconsented() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_VPN);
-
- checkProvisionVpnProfile(vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_VPN);
- }
-
- @Test
- public void testProvisionVpnProfileTooLarge() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- final VpnProfile bigProfile = new VpnProfile("");
- bigProfile.name = new String(new byte[Vpn.MAX_VPN_PROFILE_SIZE_BYTES + 1]);
-
- try {
- vpn.provisionVpnProfile(TEST_VPN_PKG, bigProfile);
- fail("Expected IAE due to profile size");
- } catch (IllegalArgumentException expected) {
- }
- }
-
- @Test
- public void testProvisionVpnProfileRestrictedUser() throws Exception {
- final Vpn vpn =
- createVpn(
- RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- try {
- vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile);
- fail("Expected SecurityException due to restricted user");
- } catch (SecurityException expected) {
- }
- }
-
- @Test
- public void testDeleteVpnProfile() throws Exception {
- final Vpn vpn = createVpn();
-
- vpn.deleteVpnProfile(TEST_VPN_PKG);
-
- verify(mVpnProfileStore)
- .remove(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
- }
-
- @Test
- public void testDeleteVpnProfileRestrictedUser() throws Exception {
- final Vpn vpn =
- createVpn(
- RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- try {
- vpn.deleteVpnProfile(TEST_VPN_PKG);
- fail("Expected SecurityException due to restricted user");
- } catch (SecurityException expected) {
- }
- }
-
- @Test
- public void testGetVpnProfilePrivileged() throws Exception {
- final Vpn vpn = createVpn();
-
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(new VpnProfile("").encode());
-
- vpn.getVpnProfilePrivileged(TEST_VPN_PKG);
-
- verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
- }
-
- private void verifyPlatformVpnIsActivated(String packageName) {
- verify(mAppOps).noteOpNoThrow(
- eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
- eq(Process.myUid()),
- eq(packageName),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- verify(mAppOps).startOp(
- eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(packageName),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- }
-
- private void verifyPlatformVpnIsDeactivated(String packageName) {
- // Add a small delay to double confirm that finishOp is only called once.
- verify(mAppOps, after(100)).finishOp(
- eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(packageName),
- eq(null) /* attributionTag */);
- }
-
- @Test
- public void testStartVpnProfile() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
-
- vpn.startVpnProfile(TEST_VPN_PKG);
-
- verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
- verifyPlatformVpnIsActivated(TEST_VPN_PKG);
- }
-
- @Test
- public void testStartVpnProfileVpnServicePreconsented() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_VPN);
-
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
-
- vpn.startVpnProfile(TEST_VPN_PKG);
-
- // Verify that the ACTIVATE_VPN appop was checked, but no error was thrown.
- verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(),
- TEST_VPN_PKG, null /* attributionTag */, null /* message */);
- }
-
- @Test
- public void testStartVpnProfileNotConsented() throws Exception {
- final Vpn vpn = createVpn();
-
- try {
- vpn.startVpnProfile(TEST_VPN_PKG);
- fail("Expected failure due to no user consent");
- } catch (SecurityException expected) {
- }
-
- // Verify both appops were checked.
- verify(mAppOps)
- .noteOpNoThrow(
- eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
- eq(Process.myUid()),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(),
- TEST_VPN_PKG, null /* attributionTag */, null /* message */);
-
- // Keystore should never have been accessed.
- verify(mVpnProfileStore, never()).get(any());
- }
-
- @Test
- public void testStartVpnProfileMissingProfile() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))).thenReturn(null);
-
- try {
- vpn.startVpnProfile(TEST_VPN_PKG);
- fail("Expected failure due to missing profile");
- } catch (IllegalArgumentException expected) {
- }
-
- verify(mVpnProfileStore).get(vpn.getProfileNameForPackage(TEST_VPN_PKG));
- verify(mAppOps)
- .noteOpNoThrow(
- eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
- eq(Process.myUid()),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- }
-
- @Test
- public void testStartVpnProfileRestrictedUser() throws Exception {
- final Vpn vpn = createVpn(RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- try {
- vpn.startVpnProfile(TEST_VPN_PKG);
- fail("Expected SecurityException due to restricted user");
- } catch (SecurityException expected) {
- }
- }
-
- @Test
- public void testStopVpnProfileRestrictedUser() throws Exception {
- final Vpn vpn = createVpn(RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
- try {
- vpn.stopVpnProfile(TEST_VPN_PKG);
- fail("Expected SecurityException due to restricted user");
- } catch (SecurityException expected) {
- }
- }
-
- @Test
- public void testStartOpAndFinishOpWillBeCalledWhenPlatformVpnIsOnAndOff() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
- vpn.startVpnProfile(TEST_VPN_PKG);
- verifyPlatformVpnIsActivated(TEST_VPN_PKG);
- // Add a small delay to make sure that startOp is only called once.
- verify(mAppOps, after(100).times(1)).startOp(
- eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- // Check that the startOp is not called with OPSTR_ESTABLISH_VPN_SERVICE.
- verify(mAppOps, never()).startOp(
- eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- vpn.stopVpnProfile(TEST_VPN_PKG);
- verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
- }
-
- @Test
- public void testStartOpWithSeamlessHandover() throws Exception {
- // Create with SYSTEM_USER so that establish() will match the user ID when checking
- // against Binder.getCallerUid
- final Vpn vpn = createVpn(SYSTEM_USER, AppOpsManager.OPSTR_ACTIVATE_VPN);
- assertTrue(vpn.prepare(TEST_VPN_PKG, null, VpnManager.TYPE_VPN_SERVICE));
- final VpnConfig config = new VpnConfig();
- config.user = "VpnTest";
- config.addresses.add(new LinkAddress("192.0.2.2/32"));
- config.mtu = 1450;
- final ResolveInfo resolveInfo = new ResolveInfo();
- final ServiceInfo serviceInfo = new ServiceInfo();
- serviceInfo.permission = BIND_VPN_SERVICE;
- resolveInfo.serviceInfo = serviceInfo;
- when(mPackageManager.resolveService(any(), anyInt())).thenReturn(resolveInfo);
- when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);
- vpn.establish(config);
- verify(mAppOps, times(1)).startOp(
- eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE),
- eq(Process.myUid()),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- // Call establish() twice with the same config, it should match seamless handover case and
- // startOp() shouldn't be called again.
- vpn.establish(config);
- verify(mAppOps, times(1)).startOp(
- eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE),
- eq(Process.myUid()),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- }
-
- private void verifyVpnManagerEvent(String sessionKey, String category, int errorClass,
- int errorCode, String[] packageName, @NonNull VpnProfileState... profileState) {
- final Context userContext =
- mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */);
- final ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
-
- final int verifyTimes = profileState.length;
- verify(userContext, timeout(TEST_TIMEOUT_MS).times(verifyTimes))
- .startService(intentArgumentCaptor.capture());
-
- for (int i = 0; i < verifyTimes; i++) {
- final Intent intent = intentArgumentCaptor.getAllValues().get(i);
- assertEquals(packageName[i], intent.getPackage());
- assertEquals(sessionKey, intent.getStringExtra(VpnManager.EXTRA_SESSION_KEY));
- final Set<String> categories = intent.getCategories();
- assertTrue(categories.contains(category));
- assertEquals(1, categories.size());
- assertEquals(errorClass,
- intent.getIntExtra(VpnManager.EXTRA_ERROR_CLASS, -1 /* defaultValue */));
- assertEquals(errorCode,
- intent.getIntExtra(VpnManager.EXTRA_ERROR_CODE, -1 /* defaultValue */));
- // CATEGORY_EVENT_DEACTIVATED_BY_USER & CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED won't
- // send NetworkCapabilities & LinkProperties to VPN app.
- // For ERROR_CODE_NETWORK_LOST, the NetworkCapabilities & LinkProperties of underlying
- // network will be cleared. So the VPN app will receive null for those 2 extra values.
- if (category.equals(VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER)
- || category.equals(VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED)
- || errorCode == VpnManager.ERROR_CODE_NETWORK_LOST) {
- assertNull(intent.getParcelableExtra(
- VpnManager.EXTRA_UNDERLYING_NETWORK_CAPABILITIES));
- assertNull(intent.getParcelableExtra(VpnManager.EXTRA_UNDERLYING_LINK_PROPERTIES));
- } else {
- assertNotNull(intent.getParcelableExtra(
- VpnManager.EXTRA_UNDERLYING_NETWORK_CAPABILITIES));
- assertNotNull(intent.getParcelableExtra(
- VpnManager.EXTRA_UNDERLYING_LINK_PROPERTIES));
- }
-
- assertEquals(profileState[i], intent.getParcelableExtra(
- VpnManager.EXTRA_VPN_PROFILE_STATE, VpnProfileState.class));
- }
- reset(userContext);
- }
-
- private void verifyDeactivatedByUser(String sessionKey, String[] packageName) {
- // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and
- // errorCode won't be set.
- verifyVpnManagerEvent(sessionKey, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
- -1 /* errorClass */, -1 /* errorCode */, packageName,
- // VPN NetworkAgnet does not switch to CONNECTED in the test, and the state is not
- // important here. Verify that the state as it is, i.e. CONNECTING state.
- new VpnProfileState(VpnProfileState.STATE_CONNECTING,
- sessionKey, false /* alwaysOn */, false /* lockdown */));
- }
-
- private void verifyAlwaysOnStateChanged(String[] packageName, VpnProfileState... profileState) {
- verifyVpnManagerEvent(null /* sessionKey */,
- VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
- -1 /* errorCode */, packageName, profileState);
- }
-
- @Test
- public void testVpnManagerEventForUserDeactivated() throws Exception {
- // For security reasons, Vpn#prepare() will check that oldPackage and newPackage are either
- // null or the package of the caller. This test will call Vpn#prepare() to pretend the old
- // VPN is replaced by a new one. But only Settings can change to some other packages, and
- // this is checked with CONTROL_VPN so simulate holding CONTROL_VPN in order to pass the
- // security checks.
- doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
-
- // Test the case that the user deactivates the vpn in vpn app.
- final String sessionKey1 = vpn.startVpnProfile(TEST_VPN_PKG);
- verifyPlatformVpnIsActivated(TEST_VPN_PKG);
- vpn.stopVpnProfile(TEST_VPN_PKG);
- verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
- verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
- reset(mDeviceIdleInternal);
- verifyDeactivatedByUser(sessionKey1, new String[] {TEST_VPN_PKG});
- reset(mAppOps);
-
- // Test the case that the user chooses another vpn and the original one is replaced.
- final String sessionKey2 = vpn.startVpnProfile(TEST_VPN_PKG);
- verifyPlatformVpnIsActivated(TEST_VPN_PKG);
- vpn.prepare(TEST_VPN_PKG, "com.new.vpn" /* newPackage */, TYPE_VPN_PLATFORM);
- verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
- verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
- reset(mDeviceIdleInternal);
- verifyDeactivatedByUser(sessionKey2, new String[] {TEST_VPN_PKG});
- }
-
- @Test
- public void testVpnManagerEventForAlwaysOnChanged() throws Exception {
- // Calling setAlwaysOnPackage() needs to hold CONTROL_VPN.
- doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- // Enable VPN always-on for PKGS[1].
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
- null /* lockdownAllowlist */));
- verifyPowerSaveTempWhitelistApp(PKGS[1]);
- reset(mDeviceIdleInternal);
- verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
- new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
- null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
-
- // Enable VPN lockdown for PKGS[1].
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true /* lockdown */,
- null /* lockdownAllowlist */));
- verifyPowerSaveTempWhitelistApp(PKGS[1]);
- reset(mDeviceIdleInternal);
- verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
- new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
- null /* sessionKey */, true /* alwaysOn */, true /* lockdown */));
-
- // Disable VPN lockdown for PKGS[1].
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
- null /* lockdownAllowlist */));
- verifyPowerSaveTempWhitelistApp(PKGS[1]);
- reset(mDeviceIdleInternal);
- verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
- new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
- null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
-
- // Disable VPN always-on.
- assertTrue(vpn.setAlwaysOnPackage(null, false /* lockdown */,
- null /* lockdownAllowlist */));
- verifyPowerSaveTempWhitelistApp(PKGS[1]);
- reset(mDeviceIdleInternal);
- verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
- new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
- null /* sessionKey */, false /* alwaysOn */, false /* lockdown */));
-
- // Enable VPN always-on for PKGS[1] again.
- assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
- null /* lockdownAllowlist */));
- verifyPowerSaveTempWhitelistApp(PKGS[1]);
- reset(mDeviceIdleInternal);
- verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
- new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
- null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
-
- // Enable VPN always-on for PKGS[2].
- assertTrue(vpn.setAlwaysOnPackage(PKGS[2], false /* lockdown */,
- null /* lockdownAllowlist */));
- verifyPowerSaveTempWhitelistApp(PKGS[2]);
- reset(mDeviceIdleInternal);
- // PKGS[1] is replaced with PKGS[2].
- // Pass 2 VpnProfileState objects to verifyVpnManagerEvent(), the first one is sent to
- // PKGS[1] to notify PKGS[1] that the VPN always-on is disabled, the second one is sent to
- // PKGS[2] to notify PKGS[2] that the VPN always-on is enabled.
- verifyAlwaysOnStateChanged(new String[] {PKGS[1], PKGS[2]},
- new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
- null /* sessionKey */, false /* alwaysOn */, false /* lockdown */),
- new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
- null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
- }
-
- @Test
- public void testReconnectVpnManagerVpnWithAlwaysOnEnabled() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
- vpn.startVpnProfile(TEST_VPN_PKG);
- verifyPlatformVpnIsActivated(TEST_VPN_PKG);
-
- // Enable VPN always-on for TEST_VPN_PKG.
- assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, false /* lockdown */,
- null /* lockdownAllowlist */));
-
- // Reset to verify next startVpnProfile.
- reset(mAppOps);
-
- vpn.stopVpnProfile(TEST_VPN_PKG);
-
- // Reconnect the vpn with different package will cause exception.
- assertThrows(SecurityException.class, () -> vpn.startVpnProfile(PKGS[0]));
-
- // Reconnect the vpn again with the vpn always on package w/o exception.
- vpn.startVpnProfile(TEST_VPN_PKG);
- verifyPlatformVpnIsActivated(TEST_VPN_PKG);
- }
-
- @Test
- public void testLockdown_enableDisableWhileConnected() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
- final InOrder order = inOrder(mTestDeps);
- order.verify(mTestDeps, timeout(TIMEOUT_CROSSTHREAD_MS))
- .newNetworkAgent(any(), any(), any(), any(), any(), any(),
- argThat(config -> config.allowBypass), any(), any());
-
- // Make VPN lockdown.
- assertTrue(vpnSnapShot.vpn.setAlwaysOnPackage(TEST_VPN_PKG, true /* lockdown */,
- null /* lockdownAllowlist */));
-
- order.verify(mTestDeps, timeout(TIMEOUT_CROSSTHREAD_MS))
- .newNetworkAgent(any(), any(), any(), any(), any(), any(),
- argThat(config -> !config.allowBypass), any(), any());
-
- // Disable lockdown.
- assertTrue(vpnSnapShot.vpn.setAlwaysOnPackage(TEST_VPN_PKG, false /* lockdown */,
- null /* lockdownAllowlist */));
-
- order.verify(mTestDeps, timeout(TIMEOUT_CROSSTHREAD_MS))
- .newNetworkAgent(any(), any(), any(), any(), any(), any(),
- argThat(config -> config.allowBypass), any(), any());
- }
-
- @Test
- public void testSetPackageAuthorizationVpnService() throws Exception {
- final Vpn vpn = createVpn();
-
- assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_SERVICE));
- verify(mAppOps)
- .setMode(
- eq(AppOpsManager.OPSTR_ACTIVATE_VPN),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(TEST_VPN_PKG),
- eq(AppOpsManager.MODE_ALLOWED));
- }
-
- @Test
- public void testSetPackageAuthorizationPlatformVpn() throws Exception {
- final Vpn vpn = createVpn();
-
- assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, TYPE_VPN_PLATFORM));
- verify(mAppOps)
- .setMode(
- eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(TEST_VPN_PKG),
- eq(AppOpsManager.MODE_ALLOWED));
- }
-
- @Test
- public void testSetPackageAuthorizationRevokeAuthorization() throws Exception {
- final Vpn vpn = createVpn();
-
- assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_NONE));
- verify(mAppOps)
- .setMode(
- eq(AppOpsManager.OPSTR_ACTIVATE_VPN),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(TEST_VPN_PKG),
- eq(AppOpsManager.MODE_IGNORED));
- verify(mAppOps)
- .setMode(
- eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
- eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
- eq(TEST_VPN_PKG),
- eq(AppOpsManager.MODE_IGNORED));
- }
-
- private NetworkCallback triggerOnAvailableAndGetCallback() throws Exception {
- return triggerOnAvailableAndGetCallback(new NetworkCapabilities.Builder().build());
- }
-
- private NetworkCallback triggerOnAvailableAndGetCallback(
- @NonNull final NetworkCapabilities caps) throws Exception {
- final ArgumentCaptor<NetworkCallback> networkCallbackCaptor =
- ArgumentCaptor.forClass(NetworkCallback.class);
- verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
- .registerSystemDefaultNetworkCallback(networkCallbackCaptor.capture(), any());
-
- // onAvailable() will trigger onDefaultNetworkChanged(), so NetdUtils#setInterfaceUp will be
- // invoked. Set the return value of INetd#interfaceGetCfg to prevent NullPointerException.
- final InterfaceConfigurationParcel config = new InterfaceConfigurationParcel();
- config.flags = new String[] {IF_STATE_DOWN};
- when(mNetd.interfaceGetCfg(anyString())).thenReturn(config);
- final NetworkCallback cb = networkCallbackCaptor.getValue();
- cb.onAvailable(TEST_NETWORK);
- // Trigger onCapabilitiesChanged() and onLinkPropertiesChanged() so the test can verify that
- // if NetworkCapabilities and LinkProperties of underlying network will be sent/cleared or
- // not.
- // See verifyVpnManagerEvent().
- cb.onCapabilitiesChanged(TEST_NETWORK, caps);
- cb.onLinkPropertiesChanged(TEST_NETWORK, new LinkProperties());
- return cb;
- }
-
- private void verifyInterfaceSetCfgWithFlags(String flag) throws Exception {
- // Add a timeout for waiting for interfaceSetCfg to be called.
- verify(mNetd, timeout(TEST_TIMEOUT_MS)).interfaceSetCfg(argThat(
- config -> Arrays.asList(config.flags).contains(flag)));
- }
-
- private void doTestPlatformVpnWithException(IkeException exception,
- String category, int errorType, int errorCode) throws Exception {
- final ArgumentCaptor<IkeSessionCallback> captor =
- ArgumentCaptor.forClass(IkeSessionCallback.class);
-
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
-
- doReturn(new NetworkCapabilities()).when(mConnectivityManager)
- .getRedactedNetworkCapabilitiesForPackage(any(), anyInt(), anyString());
- doReturn(new LinkProperties()).when(mConnectivityManager)
- .getRedactedLinkPropertiesForPackage(any(), anyInt(), anyString());
-
- final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
- final Set<Range<Integer>> uidRanges = rangeSet(PRIMARY_USER_RANGE);
- // This is triggered by Ikev2VpnRunner constructor.
- verify(mConnectivityManager, times(1)).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
- final NetworkCallback cb = triggerOnAvailableAndGetCallback();
-
- verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
-
- // Wait for createIkeSession() to be called before proceeding in order to ensure consistent
- // state
- verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
- .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
- // This is triggered by Vpn#startOrMigrateIkeSession().
- verify(mConnectivityManager, times(2)).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
- reset(mIkev2SessionCreator);
- // For network lost case, the process should be triggered by calling onLost(), which is the
- // same process with the real case.
- if (errorCode == VpnManager.ERROR_CODE_NETWORK_LOST) {
- cb.onLost(TEST_NETWORK);
- verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
- } else {
- final IkeSessionCallback ikeCb = captor.getValue();
- mExecutor.execute(() -> ikeCb.onClosedWithException(exception));
- }
-
- verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
- reset(mDeviceIdleInternal);
- verifyVpnManagerEvent(sessionKey, category, errorType, errorCode,
- // VPN NetworkAgnet does not switch to CONNECTED in the test, and the state is not
- // important here. Verify that the state as it is, i.e. CONNECTING state.
- new String[] {TEST_VPN_PKG}, new VpnProfileState(VpnProfileState.STATE_CONNECTING,
- sessionKey, false /* alwaysOn */, false /* lockdown */));
- if (errorType == VpnManager.ERROR_CLASS_NOT_RECOVERABLE) {
- verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey),
- eq(Collections.EMPTY_LIST));
- verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
- .unregisterNetworkCallback(eq(cb));
- } else if (errorType == VpnManager.ERROR_CLASS_RECOVERABLE
- // Vpn won't retry when there is no usable underlying network.
- && errorCode != VpnManager.ERROR_CODE_NETWORK_LOST) {
- int retryIndex = 0;
- // First failure occurred above.
- final IkeSessionCallback retryCb = verifyRetryAndGetNewIkeCb(retryIndex++);
- // Trigger 2 more failures to let the retry delay increase to 5s.
- mExecutor.execute(() -> retryCb.onClosedWithException(exception));
- final IkeSessionCallback retryCb2 = verifyRetryAndGetNewIkeCb(retryIndex++);
- mExecutor.execute(() -> retryCb2.onClosedWithException(exception));
- final IkeSessionCallback retryCb3 = verifyRetryAndGetNewIkeCb(retryIndex++);
-
- // setVpnDefaultForUids may be called again but the uidRanges should not change.
- verify(mConnectivityManager, atLeast(2)).setVpnDefaultForUids(eq(sessionKey),
- mUidRangesCaptor.capture());
- final List<Collection<Range<Integer>>> capturedUidRanges =
- mUidRangesCaptor.getAllValues();
- for (int i = 2; i < capturedUidRanges.size(); i++) {
- // Assert equals no order.
- assertTrue(
- "uid ranges should not be modified. Expected: " + uidRanges
- + ", actual: " + capturedUidRanges.get(i),
- capturedUidRanges.get(i).containsAll(uidRanges)
- && capturedUidRanges.get(i).size() == uidRanges.size());
- }
-
- // A fourth failure will cause the retry delay to be greater than 5s.
- mExecutor.execute(() -> retryCb3.onClosedWithException(exception));
- verifyRetryAndGetNewIkeCb(retryIndex++);
-
- // The VPN network preference will be cleared when the retry delay is greater than 5s.
- verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey),
- eq(Collections.EMPTY_LIST));
- }
- }
-
- private IkeSessionCallback verifyRetryAndGetNewIkeCb(int retryIndex) {
- final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
- ArgumentCaptor.forClass(IkeSessionCallback.class);
-
- // Verify retry is scheduled
- final long expectedDelayMs = mTestDeps.getNextRetryDelayMs(retryIndex);
- verify(mExecutor, timeout(TEST_TIMEOUT_MS)).schedule(any(Runnable.class),
- eq(expectedDelayMs), eq(TimeUnit.MILLISECONDS));
-
- verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS + expectedDelayMs))
- .createIkeSession(any(), any(), any(), any(), ikeCbCaptor.capture(), any());
-
- // Forget the mIkev2SessionCreator#createIkeSession call and mExecutor#schedule call
- // for the next retry verification
- resetIkev2SessionCreator(mIkeSessionWrapper);
-
- return ikeCbCaptor.getValue();
- }
-
- @Test
- public void testStartPlatformVpnAuthenticationFailed() throws Exception {
- final IkeProtocolException exception = mock(IkeProtocolException.class);
- final int errorCode = IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED;
- when(exception.getErrorType()).thenReturn(errorCode);
- doTestPlatformVpnWithException(exception,
- VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_NOT_RECOVERABLE,
- errorCode);
- }
-
- @Test
- public void testStartPlatformVpnFailedWithRecoverableError() throws Exception {
- final IkeProtocolException exception = mock(IkeProtocolException.class);
- final int errorCode = IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE;
- when(exception.getErrorType()).thenReturn(errorCode);
- doTestPlatformVpnWithException(exception,
- VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, errorCode);
- }
-
- @Test
- public void testStartPlatformVpnFailedWithUnknownHostException() throws Exception {
- final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
- final UnknownHostException unknownHostException = new UnknownHostException();
- final int errorCode = VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST;
- when(exception.getCause()).thenReturn(unknownHostException);
- doTestPlatformVpnWithException(exception,
- VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
- errorCode);
- }
-
- @Test
- public void testStartPlatformVpnFailedWithIkeTimeoutException() throws Exception {
- final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
- final IkeTimeoutException ikeTimeoutException =
- new IkeTimeoutException("IkeTimeoutException");
- final int errorCode = VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT;
- when(exception.getCause()).thenReturn(ikeTimeoutException);
- doTestPlatformVpnWithException(exception,
- VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
- errorCode);
- }
-
- @Test
- public void testStartPlatformVpnFailedWithIkeNetworkLostException() throws Exception {
- final IkeNetworkLostException exception = new IkeNetworkLostException(
- new Network(100));
- doTestPlatformVpnWithException(exception,
- VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
- VpnManager.ERROR_CODE_NETWORK_LOST);
- }
-
- @Test
- public void testStartPlatformVpnFailedWithIOException() throws Exception {
- final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
- final IOException ioException = new IOException();
- final int errorCode = VpnManager.ERROR_CODE_NETWORK_IO;
- when(exception.getCause()).thenReturn(ioException);
- doTestPlatformVpnWithException(exception,
- VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
- errorCode);
- }
-
- @Test
- public void testStartPlatformVpnIllegalArgumentExceptionInSetup() throws Exception {
- when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
- .thenThrow(new IllegalArgumentException());
- final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), mVpnProfile);
- final NetworkCallback cb = triggerOnAvailableAndGetCallback();
-
- verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
-
- // Wait for createIkeSession() to be called before proceeding in order to ensure consistent
- // state
- verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)).unregisterNetworkCallback(eq(cb));
- assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state);
- }
-
- @Test
- public void testVpnManagerEventWillNotBeSentToSettingsVpn() throws Exception {
- startLegacyVpn(createVpn(PRIMARY_USER.id), mVpnProfile);
- triggerOnAvailableAndGetCallback();
-
- verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
-
- final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
- final IkeTimeoutException ikeTimeoutException =
- new IkeTimeoutException("IkeTimeoutException");
- when(exception.getCause()).thenReturn(ikeTimeoutException);
-
- final ArgumentCaptor<IkeSessionCallback> captor =
- ArgumentCaptor.forClass(IkeSessionCallback.class);
- verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
- .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
- final IkeSessionCallback ikeCb = captor.getValue();
- ikeCb.onClosedWithException(exception);
-
- final Context userContext =
- mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */);
- verify(userContext, never()).startService(any());
- }
-
- private void setAndVerifyAlwaysOnPackage(Vpn vpn, int uid, boolean lockdownEnabled) {
- assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, lockdownEnabled, null));
-
- verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
- verify(mAppOps).setMode(
- eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), eq(uid), eq(TEST_VPN_PKG),
- eq(AppOpsManager.MODE_ALLOWED));
-
- verify(mSystemServices).settingsSecurePutStringForUser(
- eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(PRIMARY_USER.id));
- verify(mSystemServices).settingsSecurePutIntForUser(
- eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN), eq(lockdownEnabled ? 1 : 0),
- eq(PRIMARY_USER.id));
- verify(mSystemServices).settingsSecurePutStringForUser(
- eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(PRIMARY_USER.id));
- }
-
- @Test
- public void testSetAndStartAlwaysOnVpn() throws Exception {
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- setMockedUsers(PRIMARY_USER);
-
- // UID checks must return a different UID; otherwise it'll be treated as already prepared.
- final int uid = Process.myUid() + 1;
- when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt()))
- .thenReturn(uid);
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(mVpnProfile.encode());
-
- setAndVerifyAlwaysOnPackage(vpn, uid, false);
- assertTrue(vpn.startAlwaysOnVpn());
-
- // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in
- // a subsequent CL.
- }
-
- private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
- setMockedUsers(PRIMARY_USER);
- vpn.startLegacyVpn(vpnProfile);
- return vpn;
- }
-
- private IkeSessionConnectionInfo createIkeConnectInfo() {
- return new IkeSessionConnectionInfo(TEST_VPN_CLIENT_IP, TEST_VPN_SERVER_IP, TEST_NETWORK);
- }
-
- private IkeSessionConnectionInfo createIkeConnectInfo_2() {
- return new IkeSessionConnectionInfo(
- TEST_VPN_CLIENT_IP_2, TEST_VPN_SERVER_IP_2, TEST_NETWORK_2);
- }
-
- private IkeSessionConfiguration createIkeConfig(
- IkeSessionConnectionInfo ikeConnectInfo, boolean isMobikeEnabled) {
- final IkeSessionConfiguration.Builder builder =
- new IkeSessionConfiguration.Builder(ikeConnectInfo);
-
- if (isMobikeEnabled) {
- builder.addIkeExtension(EXTENSION_TYPE_MOBIKE);
- }
-
- return builder.build();
- }
-
- private ChildSessionConfiguration createChildConfig() {
- return new ChildSessionConfiguration.Builder(
- Arrays.asList(IN_TS, IN_TS6), Arrays.asList(OUT_TS, OUT_TS6))
- .addInternalAddress(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN))
- .addInternalAddress(new LinkAddress(TEST_VPN_INTERNAL_IP6, IP6_PREFIX_LEN))
- .addInternalDnsServer(TEST_VPN_INTERNAL_DNS)
- .addInternalDnsServer(TEST_VPN_INTERNAL_DNS6)
- .build();
- }
-
- private IpSecTransform createIpSecTransform() {
- return new IpSecTransform(mContext, new IpSecConfig());
- }
-
- private void verifyApplyTunnelModeTransforms(int expectedTimes) throws Exception {
- verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
- eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_IN),
- anyInt(), anyString());
- verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
- eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_OUT),
- anyInt(), anyString());
- }
-
- private Pair<IkeSessionCallback, ChildSessionCallback> verifyCreateIkeAndCaptureCbs()
- throws Exception {
- final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
- ArgumentCaptor.forClass(IkeSessionCallback.class);
- final ArgumentCaptor<ChildSessionCallback> childCbCaptor =
- ArgumentCaptor.forClass(ChildSessionCallback.class);
-
- verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS)).createIkeSession(
- any(), any(), any(), any(), ikeCbCaptor.capture(), childCbCaptor.capture());
-
- return new Pair<>(ikeCbCaptor.getValue(), childCbCaptor.getValue());
- }
-
- private static class PlatformVpnSnapshot {
- public final Vpn vpn;
- public final NetworkCallback nwCb;
- public final IkeSessionCallback ikeCb;
- public final ChildSessionCallback childCb;
-
- PlatformVpnSnapshot(Vpn vpn, NetworkCallback nwCb,
- IkeSessionCallback ikeCb, ChildSessionCallback childCb) {
- this.vpn = vpn;
- this.nwCb = nwCb;
- this.ikeCb = ikeCb;
- this.childCb = childCb;
- }
- }
-
- private PlatformVpnSnapshot verifySetupPlatformVpn(IkeSessionConfiguration ikeConfig)
- throws Exception {
- return verifySetupPlatformVpn(ikeConfig, true);
- }
-
- private PlatformVpnSnapshot verifySetupPlatformVpn(
- IkeSessionConfiguration ikeConfig, boolean mtuSupportsIpv6) throws Exception {
- return verifySetupPlatformVpn(mVpnProfile, ikeConfig, mtuSupportsIpv6);
- }
-
- private PlatformVpnSnapshot verifySetupPlatformVpn(VpnProfile vpnProfile,
- IkeSessionConfiguration ikeConfig, boolean mtuSupportsIpv6) throws Exception {
- return verifySetupPlatformVpn(vpnProfile, ikeConfig,
- new NetworkCapabilities.Builder().build() /* underlying network caps */,
- mtuSupportsIpv6, false /* areLongLivedTcpConnectionsExpensive */);
- }
-
- private PlatformVpnSnapshot verifySetupPlatformVpn(VpnProfile vpnProfile,
- IkeSessionConfiguration ikeConfig,
- @NonNull final NetworkCapabilities underlyingNetworkCaps,
- boolean mtuSupportsIpv6,
- boolean areLongLivedTcpConnectionsExpensive) throws Exception {
- if (!mtuSupportsIpv6) {
- doReturn(IPV6_MIN_MTU - 1).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(),
- anyBoolean());
- }
-
- doReturn(mMockNetworkAgent).when(mTestDeps)
- .newNetworkAgent(
- any(), any(), anyString(), any(), any(), any(), any(), any(), any());
- doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
-
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
- when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
- .thenReturn(vpnProfile.encode());
-
- final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
- final Set<Range<Integer>> uidRanges = Collections.singleton(PRIMARY_USER_RANGE);
- verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
- final NetworkCallback nwCb = triggerOnAvailableAndGetCallback(underlyingNetworkCaps);
- // There are 4 interactions with the executor.
- // - Network available
- // - LP change
- // - NC change
- // - schedule() calls in scheduleStartIkeSession()
- // The first 3 calls are triggered from Executor.execute(). The execute() will also call to
- // schedule() with 0 delay. Verify the exact interaction here so that it won't cause flakes
- // in the follow-up flow.
- verify(mExecutor, timeout(TEST_TIMEOUT_MS).times(4))
- .schedule(any(Runnable.class), anyLong(), any());
- reset(mExecutor);
-
- // Mock the setup procedure by firing callbacks
- final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
- verifyCreateIkeAndCaptureCbs();
- final IkeSessionCallback ikeCb = cbPair.first;
- final ChildSessionCallback childCb = cbPair.second;
-
- ikeCb.onOpened(ikeConfig);
- childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
- childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
- childCb.onOpened(createChildConfig());
-
- // Verification VPN setup
- verifyApplyTunnelModeTransforms(1);
-
- ArgumentCaptor<LinkProperties> lpCaptor = ArgumentCaptor.forClass(LinkProperties.class);
- ArgumentCaptor<NetworkCapabilities> ncCaptor =
- ArgumentCaptor.forClass(NetworkCapabilities.class);
- ArgumentCaptor<NetworkAgentConfig> nacCaptor =
- ArgumentCaptor.forClass(NetworkAgentConfig.class);
- verify(mTestDeps).newNetworkAgent(
- any(), any(), anyString(), ncCaptor.capture(), lpCaptor.capture(),
- any(), nacCaptor.capture(), any(), any());
- verify(mIkeSessionWrapper).setUnderpinnedNetwork(TEST_NETWORK);
- // Check LinkProperties
- final LinkProperties lp = lpCaptor.getValue();
- final List<RouteInfo> expectedRoutes =
- new ArrayList<>(
- Arrays.asList(
- new RouteInfo(
- new IpPrefix(Inet4Address.ANY, 0),
- null /* gateway */,
- TEST_IFACE_NAME,
- RouteInfo.RTN_UNICAST)));
- final List<LinkAddress> expectedAddresses =
- new ArrayList<>(
- Arrays.asList(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN)));
- final List<InetAddress> expectedDns = new ArrayList<>(Arrays.asList(TEST_VPN_INTERNAL_DNS));
-
- if (mtuSupportsIpv6) {
- expectedRoutes.add(
- new RouteInfo(
- new IpPrefix(Inet6Address.ANY, 0),
- null /* gateway */,
- TEST_IFACE_NAME,
- RouteInfo.RTN_UNICAST));
- expectedAddresses.add(new LinkAddress(TEST_VPN_INTERNAL_IP6, IP6_PREFIX_LEN));
- expectedDns.add(TEST_VPN_INTERNAL_DNS6);
- } else {
- expectedRoutes.add(
- new RouteInfo(
- new IpPrefix(Inet6Address.ANY, 0),
- null /* gateway */,
- TEST_IFACE_NAME,
- RTN_UNREACHABLE));
- }
-
- assertEquals(expectedRoutes, lp.getRoutes());
- assertEquals(expectedAddresses, lp.getLinkAddresses());
- assertEquals(expectedDns, lp.getDnsServers());
-
- // Check NetworkCapabilities
- assertEquals(Arrays.asList(TEST_NETWORK), ncCaptor.getValue().getUnderlyingNetworks());
-
- // Check if allowBypass is set or not.
- assertTrue(nacCaptor.getValue().isBypassableVpn());
- // Check if extra info for VPN is set.
- assertTrue(nacCaptor.getValue().getLegacyExtraInfo().contains(TEST_VPN_PKG));
- final VpnTransportInfo info = (VpnTransportInfo) ncCaptor.getValue().getTransportInfo();
- assertTrue(info.isBypassable());
- assertEquals(areLongLivedTcpConnectionsExpensive,
- info.areLongLivedTcpConnectionsExpensive());
- return new PlatformVpnSnapshot(vpn, nwCb, ikeCb, childCb);
- }
-
- @Test
- public void testStartPlatformVpn() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- verify(mConnectivityManager).setVpnDefaultForUids(anyString(), eq(Collections.EMPTY_LIST));
- }
-
- @Test
- public void testMigrateIkeSession_FromIkeTunnConnParams_AutoTimerNoTimer() throws Exception {
- doTestMigrateIkeSession_FromIkeTunnConnParams(
- false /* isAutomaticIpVersionSelectionEnabled */,
- true /* isAutomaticNattKeepaliveTimerEnabled */,
- TEST_KEEPALIVE_TIMEOUT_UNSET /* keepaliveInProfile */,
- ESP_IP_VERSION_AUTO /* ipVersionInProfile */,
- ESP_ENCAP_TYPE_AUTO /* encapTypeInProfile */);
- }
-
- @Test
- public void testMigrateIkeSession_FromIkeTunnConnParams_AutoTimerTimerSet() throws Exception {
- doTestMigrateIkeSession_FromIkeTunnConnParams(
- false /* isAutomaticIpVersionSelectionEnabled */,
- true /* isAutomaticNattKeepaliveTimerEnabled */,
- TEST_KEEPALIVE_TIMER /* keepaliveInProfile */,
- ESP_IP_VERSION_AUTO /* ipVersionInProfile */,
- ESP_ENCAP_TYPE_AUTO /* encapTypeInProfile */);
- }
-
- @Test
- public void testMigrateIkeSession_FromIkeTunnConnParams_AutoIp() throws Exception {
- doTestMigrateIkeSession_FromIkeTunnConnParams(
- true /* isAutomaticIpVersionSelectionEnabled */,
- false /* isAutomaticNattKeepaliveTimerEnabled */,
- TEST_KEEPALIVE_TIMEOUT_UNSET /* keepaliveInProfile */,
- ESP_IP_VERSION_AUTO /* ipVersionInProfile */,
- ESP_ENCAP_TYPE_AUTO /* encapTypeInProfile */);
- }
-
- @Test
- public void testMigrateIkeSession_FromIkeTunnConnParams_AssignedIpProtocol() throws Exception {
- doTestMigrateIkeSession_FromIkeTunnConnParams(
- false /* isAutomaticIpVersionSelectionEnabled */,
- false /* isAutomaticNattKeepaliveTimerEnabled */,
- TEST_KEEPALIVE_TIMEOUT_UNSET /* keepaliveInProfile */,
- ESP_IP_VERSION_IPV4 /* ipVersionInProfile */,
- ESP_ENCAP_TYPE_UDP /* encapTypeInProfile */);
- }
-
- @Test
- public void testMigrateIkeSession_FromNotIkeTunnConnParams_AutoTimer() throws Exception {
- doTestMigrateIkeSession_FromNotIkeTunnConnParams(
- false /* isAutomaticIpVersionSelectionEnabled */,
- true /* isAutomaticNattKeepaliveTimerEnabled */);
- }
-
- @Test
- public void testMigrateIkeSession_FromNotIkeTunnConnParams_AutoIp() throws Exception {
- doTestMigrateIkeSession_FromNotIkeTunnConnParams(
- true /* isAutomaticIpVersionSelectionEnabled */,
- false /* isAutomaticNattKeepaliveTimerEnabled */);
- }
-
- private void doTestMigrateIkeSession_FromNotIkeTunnConnParams(
- boolean isAutomaticIpVersionSelectionEnabled,
- boolean isAutomaticNattKeepaliveTimerEnabled) throws Exception {
- final Ikev2VpnProfile ikeProfile =
- new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
- .setAuthPsk(TEST_VPN_PSK)
- .setBypassable(true /* isBypassable */)
- .setAutomaticNattKeepaliveTimerEnabled(isAutomaticNattKeepaliveTimerEnabled)
- .setAutomaticIpVersionSelectionEnabled(isAutomaticIpVersionSelectionEnabled)
- .build();
-
- final int expectedKeepalive = isAutomaticNattKeepaliveTimerEnabled
- ? AUTOMATIC_KEEPALIVE_DELAY_SECONDS
- : DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT;
- doTestMigrateIkeSession(ikeProfile.toVpnProfile(),
- expectedKeepalive,
- ESP_IP_VERSION_AUTO /* expectedIpVersion */,
- ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
- new NetworkCapabilities.Builder().build());
- }
-
- private Ikev2VpnProfile makeIkeV2VpnProfile(
- boolean isAutomaticIpVersionSelectionEnabled,
- boolean isAutomaticNattKeepaliveTimerEnabled,
- int keepaliveInProfile,
- int ipVersionInProfile,
- int encapTypeInProfile) {
- // TODO: Update helper function in IkeSessionTestUtils to support building IkeSessionParams
- // with IP version and encap type when mainline-prod branch support these two APIs.
- final IkeSessionParams params = getTestIkeSessionParams(true /* testIpv6 */,
- new IkeFqdnIdentification(TEST_IDENTITY), keepaliveInProfile);
- final IkeSessionParams ikeSessionParams = new IkeSessionParams.Builder(params)
- .setIpVersion(ipVersionInProfile)
- .setEncapType(encapTypeInProfile)
- .build();
-
- final IkeTunnelConnectionParams tunnelParams =
- new IkeTunnelConnectionParams(ikeSessionParams, CHILD_PARAMS);
- return new Ikev2VpnProfile.Builder(tunnelParams)
- .setBypassable(true)
- .setAutomaticNattKeepaliveTimerEnabled(isAutomaticNattKeepaliveTimerEnabled)
- .setAutomaticIpVersionSelectionEnabled(isAutomaticIpVersionSelectionEnabled)
- .build();
- }
-
- private void doTestMigrateIkeSession_FromIkeTunnConnParams(
- boolean isAutomaticIpVersionSelectionEnabled,
- boolean isAutomaticNattKeepaliveTimerEnabled,
- int keepaliveInProfile,
- int ipVersionInProfile,
- int encapTypeInProfile) throws Exception {
- doTestMigrateIkeSession_FromIkeTunnConnParams(isAutomaticIpVersionSelectionEnabled,
- isAutomaticNattKeepaliveTimerEnabled, keepaliveInProfile, ipVersionInProfile,
- encapTypeInProfile, new NetworkCapabilities.Builder().build());
- }
-
- private void doTestMigrateIkeSession_FromIkeTunnConnParams(
- boolean isAutomaticIpVersionSelectionEnabled,
- boolean isAutomaticNattKeepaliveTimerEnabled,
- int keepaliveInProfile,
- int ipVersionInProfile,
- int encapTypeInProfile,
- @NonNull final NetworkCapabilities nc) throws Exception {
- final Ikev2VpnProfile ikeProfile = makeIkeV2VpnProfile(
- isAutomaticIpVersionSelectionEnabled,
- isAutomaticNattKeepaliveTimerEnabled,
- keepaliveInProfile,
- ipVersionInProfile,
- encapTypeInProfile);
-
- final IkeSessionParams ikeSessionParams =
- ikeProfile.getIkeTunnelConnectionParams().getIkeSessionParams();
- final int expectedKeepalive = isAutomaticNattKeepaliveTimerEnabled
- ? AUTOMATIC_KEEPALIVE_DELAY_SECONDS
- : ikeSessionParams.getNattKeepAliveDelaySeconds();
- final int expectedIpVersion = isAutomaticIpVersionSelectionEnabled
- ? ESP_IP_VERSION_AUTO
- : ikeSessionParams.getIpVersion();
- final int expectedEncapType = isAutomaticIpVersionSelectionEnabled
- ? ESP_ENCAP_TYPE_AUTO
- : ikeSessionParams.getEncapType();
- doTestMigrateIkeSession(ikeProfile.toVpnProfile(), expectedKeepalive,
- expectedIpVersion, expectedEncapType, nc);
- }
-
- @Test
- public void doTestMigrateIkeSession_Vcn() throws Exception {
- final int expectedKeepalive = 2097; // Any unlikely number will do
- final NetworkCapabilities vcnNc = new NetworkCapabilities.Builder()
- .addTransportType(TRANSPORT_CELLULAR)
- .setTransportInfo(new VcnTransportInfo(TEST_SUB_ID, expectedKeepalive))
- .build();
- final Ikev2VpnProfile ikev2VpnProfile = makeIkeV2VpnProfile(
- true /* isAutomaticIpVersionSelectionEnabled */,
- true /* isAutomaticNattKeepaliveTimerEnabled */,
- 234 /* keepaliveInProfile */, // Should be ignored, any value will do
- ESP_IP_VERSION_IPV4, // Should be ignored
- ESP_ENCAP_TYPE_UDP // Should be ignored
- );
- doTestMigrateIkeSession(
- ikev2VpnProfile.toVpnProfile(),
- expectedKeepalive,
- ESP_IP_VERSION_AUTO /* expectedIpVersion */,
- ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
- vcnNc);
- }
-
- private void doTestMigrateIkeSession(
- @NonNull final VpnProfile profile,
- final int expectedKeepalive,
- final int expectedIpVersion,
- final int expectedEncapType,
- @NonNull final NetworkCapabilities caps) throws Exception {
- final PlatformVpnSnapshot vpnSnapShot =
- verifySetupPlatformVpn(profile,
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
- caps /* underlying network capabilities */,
- false /* mtuSupportsIpv6 */,
- expectedKeepalive < DEFAULT_LONG_LIVED_TCP_CONNS_EXPENSIVE_TIMEOUT_SEC);
- // Simulate a new network coming up
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
- verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt());
-
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2, caps);
- // Verify MOBIKE is triggered
- verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(TEST_NETWORK_2,
- expectedIpVersion, expectedEncapType, expectedKeepalive);
-
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- }
-
- @Test
- public void testLinkPropertiesUpdateTriggerReevaluation() throws Exception {
- final boolean hasV6 = true;
-
- mockCarrierConfig(TEST_SUB_ID, TelephonyManager.SIM_STATE_LOADED, TEST_KEEPALIVE_TIMER,
- PREFERRED_IKE_PROTOCOL_IPV6_ESP);
- final IkeSessionParams params = getTestIkeSessionParams(hasV6,
- new IkeFqdnIdentification(TEST_IDENTITY), TEST_KEEPALIVE_TIMER);
- final IkeTunnelConnectionParams tunnelParams =
- new IkeTunnelConnectionParams(params, CHILD_PARAMS);
- final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams)
- .setBypassable(true)
- .setAutomaticNattKeepaliveTimerEnabled(false)
- .setAutomaticIpVersionSelectionEnabled(true)
- .build();
- final PlatformVpnSnapshot vpnSnapShot =
- verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
- new NetworkCapabilities.Builder().build() /* underlying network caps */,
- hasV6 /* mtuSupportsIpv6 */,
- false /* areLongLivedTcpConnectionsExpensive */);
- reset(mExecutor);
-
- // Simulate a new network coming up
- final LinkProperties lp = new LinkProperties();
- lp.addLinkAddress(new LinkAddress("192.0.2.2/32"));
-
- // Have the executor use the real delay to make sure schedule() was called only
- // once for all calls. Also, arrange for execute() not to call schedule() to avoid
- // messing with the checks for schedule().
- mExecutor.delayMs = TestExecutor.REAL_DELAY;
- mExecutor.executeDirect = true;
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
- vpnSnapShot.nwCb.onCapabilitiesChanged(
- TEST_NETWORK_2, new NetworkCapabilities.Builder().build());
- vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
- verify(mExecutor).schedule(any(Runnable.class), longThat(it -> it > 0), any());
- reset(mExecutor);
-
- final InOrder order = inOrder(mIkeSessionWrapper);
-
- // Verify the network is started
- order.verify(mIkeSessionWrapper, timeout(TIMEOUT_CROSSTHREAD_MS)).setNetwork(TEST_NETWORK_2,
- ESP_IP_VERSION_AUTO, ESP_ENCAP_TYPE_AUTO, TEST_KEEPALIVE_TIMER);
-
- // Send the same properties, check that no migration is scheduled
- vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
- verify(mExecutor, never()).schedule(any(Runnable.class), anyLong(), any());
-
- // Add v6 address, verify MOBIKE is triggered
- lp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
- vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
- order.verify(mIkeSessionWrapper, timeout(TIMEOUT_CROSSTHREAD_MS)).setNetwork(TEST_NETWORK_2,
- ESP_IP_VERSION_AUTO, ESP_ENCAP_TYPE_AUTO, TEST_KEEPALIVE_TIMER);
-
- // Add another v4 address, verify MOBIKE is triggered
- final LinkProperties stacked = new LinkProperties();
- stacked.setInterfaceName("v4-" + lp.getInterfaceName());
- stacked.addLinkAddress(new LinkAddress("192.168.0.1/32"));
- lp.addStackedLink(stacked);
- vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
- order.verify(mIkeSessionWrapper, timeout(TIMEOUT_CROSSTHREAD_MS)).setNetwork(TEST_NETWORK_2,
- ESP_IP_VERSION_AUTO, ESP_ENCAP_TYPE_AUTO, TEST_KEEPALIVE_TIMER);
-
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- }
-
- private void mockCarrierConfig(int subId, int simStatus, int keepaliveTimer, int ikeProtocol) {
- final SubscriptionInfo subscriptionInfo = mock(SubscriptionInfo.class);
- doReturn(subId).when(subscriptionInfo).getSubscriptionId();
- doReturn(List.of(subscriptionInfo)).when(mSubscriptionManager)
- .getActiveSubscriptionInfoList();
-
- doReturn(simStatus).when(mTmPerSub).getSimApplicationState();
- doReturn(TEST_MCCMNC).when(mTmPerSub).getSimOperator(subId);
-
- final PersistableBundle persistableBundle = new PersistableBundle();
- persistableBundle.putInt(KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT, keepaliveTimer);
- persistableBundle.putInt(KEY_PREFERRED_IKE_PROTOCOL_INT, ikeProtocol);
- // For CarrierConfigManager.isConfigForIdentifiedCarrier check
- persistableBundle.putBoolean(KEY_CARRIER_CONFIG_APPLIED_BOOL, true);
- doReturn(persistableBundle).when(mConfigManager).getConfigForSubId(subId);
- }
-
- private CarrierConfigManager.CarrierConfigChangeListener getCarrierConfigListener() {
- final ArgumentCaptor<CarrierConfigManager.CarrierConfigChangeListener> listenerCaptor =
- ArgumentCaptor.forClass(CarrierConfigManager.CarrierConfigChangeListener.class);
-
- verify(mConfigManager).registerCarrierConfigChangeListener(any(), listenerCaptor.capture());
-
- return listenerCaptor.getValue();
- }
-
- @Test
- public void testNattKeepaliveTimerFromCarrierConfig_noSubId() throws Exception {
- doTestReadCarrierConfig(new NetworkCapabilities(),
- TelephonyManager.SIM_STATE_LOADED,
- PREFERRED_IKE_PROTOCOL_IPV4_UDP,
- AUTOMATIC_KEEPALIVE_DELAY_SECONDS /* expectedKeepaliveTimer */,
- ESP_IP_VERSION_AUTO /* expectedIpVersion */,
- ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
- false /* expectedReadFromCarrierConfig*/,
- true /* areLongLivedTcpConnectionsExpensive */);
- }
-
- @Test
- public void testNattKeepaliveTimerFromCarrierConfig_simAbsent() throws Exception {
- doTestReadCarrierConfig(new NetworkCapabilities.Builder().build(),
- TelephonyManager.SIM_STATE_ABSENT,
- PREFERRED_IKE_PROTOCOL_IPV4_UDP,
- AUTOMATIC_KEEPALIVE_DELAY_SECONDS /* expectedKeepaliveTimer */,
- ESP_IP_VERSION_AUTO /* expectedIpVersion */,
- ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
- false /* expectedReadFromCarrierConfig*/,
- true /* areLongLivedTcpConnectionsExpensive */);
- }
-
- @Test
- public void testNattKeepaliveTimerFromCarrierConfig() throws Exception {
- doTestReadCarrierConfig(createTestCellNc(),
- TelephonyManager.SIM_STATE_LOADED,
- PREFERRED_IKE_PROTOCOL_AUTO,
- TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
- ESP_IP_VERSION_AUTO /* expectedIpVersion */,
- ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
- true /* expectedReadFromCarrierConfig*/,
- false /* areLongLivedTcpConnectionsExpensive */);
- }
-
- @Test
- public void testNattKeepaliveTimerFromCarrierConfig_NotCell() throws Exception {
- final NetworkCapabilities nc = new NetworkCapabilities.Builder()
- .addTransportType(TRANSPORT_WIFI)
- .setTransportInfo(new WifiInfo.Builder().build())
- .build();
- doTestReadCarrierConfig(nc,
- TelephonyManager.SIM_STATE_LOADED,
- PREFERRED_IKE_PROTOCOL_IPV4_UDP,
- AUTOMATIC_KEEPALIVE_DELAY_SECONDS /* expectedKeepaliveTimer */,
- ESP_IP_VERSION_AUTO /* expectedIpVersion */,
- ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
- false /* expectedReadFromCarrierConfig*/,
- true /* areLongLivedTcpConnectionsExpensive */);
- }
-
- @Test
- public void testPreferredIpProtocolFromCarrierConfig_v4UDP() throws Exception {
- doTestReadCarrierConfig(createTestCellNc(),
- TelephonyManager.SIM_STATE_LOADED,
- PREFERRED_IKE_PROTOCOL_IPV4_UDP,
- TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
- ESP_IP_VERSION_IPV4 /* expectedIpVersion */,
- ESP_ENCAP_TYPE_UDP /* expectedEncapType */,
- true /* expectedReadFromCarrierConfig*/,
- false /* areLongLivedTcpConnectionsExpensive */);
- }
-
- @Test
- public void testPreferredIpProtocolFromCarrierConfig_v6ESP() throws Exception {
- doTestReadCarrierConfig(createTestCellNc(),
- TelephonyManager.SIM_STATE_LOADED,
- PREFERRED_IKE_PROTOCOL_IPV6_ESP,
- TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
- ESP_IP_VERSION_IPV6 /* expectedIpVersion */,
- ESP_ENCAP_TYPE_NONE /* expectedEncapType */,
- true /* expectedReadFromCarrierConfig*/,
- false /* areLongLivedTcpConnectionsExpensive */);
- }
-
- @Test
- public void testPreferredIpProtocolFromCarrierConfig_v6UDP() throws Exception {
- doTestReadCarrierConfig(createTestCellNc(),
- TelephonyManager.SIM_STATE_LOADED,
- PREFERRED_IKE_PROTOCOL_IPV6_UDP,
- TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
- ESP_IP_VERSION_IPV6 /* expectedIpVersion */,
- ESP_ENCAP_TYPE_UDP /* expectedEncapType */,
- true /* expectedReadFromCarrierConfig*/,
- false /* areLongLivedTcpConnectionsExpensive */);
- }
-
- private NetworkCapabilities createTestCellNc() {
- return new NetworkCapabilities.Builder()
- .addTransportType(TRANSPORT_CELLULAR)
- .setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder()
- .setSubscriptionId(TEST_SUB_ID)
- .build())
- .build();
- }
-
- private void doTestReadCarrierConfig(NetworkCapabilities nc, int simState, int preferredIpProto,
- int expectedKeepaliveTimer, int expectedIpVersion, int expectedEncapType,
- boolean expectedReadFromCarrierConfig,
- boolean areLongLivedTcpConnectionsExpensive)
- throws Exception {
- final Ikev2VpnProfile ikeProfile =
- new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
- .setAuthPsk(TEST_VPN_PSK)
- .setBypassable(true /* isBypassable */)
- .setAutomaticNattKeepaliveTimerEnabled(true)
- .setAutomaticIpVersionSelectionEnabled(true)
- .build();
-
- final PlatformVpnSnapshot vpnSnapShot =
- verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
- new NetworkCapabilities.Builder().build() /* underlying network caps */,
- false /* mtuSupportsIpv6 */,
- true /* areLongLivedTcpConnectionsExpensive */);
-
- final CarrierConfigManager.CarrierConfigChangeListener listener =
- getCarrierConfigListener();
-
- // Simulate a new network coming up
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
- // Migration will not be started until receiving network capabilities change.
- verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt());
-
- reset(mIkeSessionWrapper);
- mockCarrierConfig(TEST_SUB_ID, simState, TEST_KEEPALIVE_TIMER, preferredIpProto);
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2, nc);
- verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(TEST_NETWORK_2,
- expectedIpVersion, expectedEncapType, expectedKeepaliveTimer);
- if (expectedReadFromCarrierConfig) {
- final ArgumentCaptor<NetworkCapabilities> ncCaptor =
- ArgumentCaptor.forClass(NetworkCapabilities.class);
- verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS))
- .doSendNetworkCapabilities(ncCaptor.capture());
-
- final VpnTransportInfo info =
- (VpnTransportInfo) ncCaptor.getValue().getTransportInfo();
- assertEquals(areLongLivedTcpConnectionsExpensive,
- info.areLongLivedTcpConnectionsExpensive());
- } else {
- verify(mMockNetworkAgent, never()).doSendNetworkCapabilities(any());
- }
-
- reset(mExecutor);
- reset(mIkeSessionWrapper);
- reset(mMockNetworkAgent);
-
- // Trigger carrier config change
- listener.onCarrierConfigChanged(1 /* logicalSlotIndex */, TEST_SUB_ID,
- -1 /* carrierId */, -1 /* specificCarrierId */);
- verify(mIkeSessionWrapper).setNetwork(TEST_NETWORK_2,
- expectedIpVersion, expectedEncapType, expectedKeepaliveTimer);
- // Expect no NetworkCapabilities change.
- // Call to doSendNetworkCapabilities() will not be triggered.
- verify(mMockNetworkAgent, never()).doSendNetworkCapabilities(any());
- }
-
- @Test
- public void testStartPlatformVpn_mtuDoesNotSupportIpv6() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot =
- verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
- false /* mtuSupportsIpv6 */);
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- }
-
- @Test
- public void testStartPlatformVpn_underlyingNetworkNotChange() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
- // Trigger update on the same network should not cause underlying network change in NC of
- // the VPN network
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK);
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK,
- new NetworkCapabilities.Builder()
- .setSubscriptionIds(Set.of(TEST_SUB_ID))
- .build());
- // Verify setNetwork() called but no underlying network update
- verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK),
- eq(ESP_IP_VERSION_AUTO) /* ipVersion */,
- eq(ESP_ENCAP_TYPE_AUTO) /* encapType */,
- eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */);
- verify(mMockNetworkAgent, never())
- .doSetUnderlyingNetworks(any());
-
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2,
- new NetworkCapabilities.Builder().build());
-
- // A new network should trigger both setNetwork() and a underlying network update.
- verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK_2),
- eq(ESP_IP_VERSION_AUTO) /* ipVersion */,
- eq(ESP_ENCAP_TYPE_AUTO) /* encapType */,
- eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */);
- verify(mMockNetworkAgent).doSetUnderlyingNetworks(
- Collections.singletonList(TEST_NETWORK_2));
-
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- }
-
- @Test
- public void testStartPlatformVpnMobility_mobikeEnabled() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
- // Set new MTU on a different network
- final int newMtu = IPV6_MIN_MTU + 1;
- doReturn(newMtu).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(), anyBoolean());
-
- // Mock network loss and verify a cleanup task is scheduled
- vpnSnapShot.nwCb.onLost(TEST_NETWORK);
- verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
-
- // Mock new network comes up and the cleanup task is cancelled
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
- verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt());
-
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2,
- new NetworkCapabilities.Builder().build());
- // Verify MOBIKE is triggered
- verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK_2),
- eq(ESP_IP_VERSION_AUTO) /* ipVersion */,
- eq(ESP_ENCAP_TYPE_AUTO) /* encapType */,
- eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */);
- // Verify mNetworkCapabilities is updated
- assertEquals(
- Collections.singletonList(TEST_NETWORK_2),
- vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
- verify(mMockNetworkAgent)
- .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
-
- // Mock the MOBIKE procedure
- vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2());
- vpnSnapShot.childCb.onIpSecTransformsMigrated(
- createIpSecTransform(), createIpSecTransform());
-
- verify(mIpSecService).setNetworkForTunnelInterface(
- eq(TEST_TUNNEL_RESOURCE_ID), eq(TEST_NETWORK_2), anyString());
-
- // Expect 2 times: one for initial setup and one for MOBIKE
- verifyApplyTunnelModeTransforms(2);
-
- // Verify mNetworkAgent is updated
- verify(mMockNetworkAgent).doSendLinkProperties(argThat(lp -> lp.getMtu() == newMtu));
- verify(mMockNetworkAgent, never()).unregister();
- // No further doSetUnderlyingNetworks interaction. The interaction count should stay one.
- verify(mMockNetworkAgent, times(1)).doSetUnderlyingNetworks(any());
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- }
-
- @Test
- public void testStartPlatformVpnMobility_mobikeEnabledMtuDoesNotSupportIpv6() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot =
- verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
- // Set MTU below 1280
- final int newMtu = IPV6_MIN_MTU - 1;
- doReturn(newMtu).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(), anyBoolean());
-
- // Mock new network available & MOBIKE procedures
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2,
- new NetworkCapabilities.Builder().build());
- // Verify mNetworkCapabilities is updated
- verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS))
- .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
- assertEquals(
- Collections.singletonList(TEST_NETWORK_2),
- vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
-
- vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2());
- vpnSnapShot.childCb.onIpSecTransformsMigrated(
- createIpSecTransform(), createIpSecTransform());
-
- // Verify removal of IPv6 addresses and routes triggers a network agent restart
- final ArgumentCaptor<LinkProperties> lpCaptor =
- ArgumentCaptor.forClass(LinkProperties.class);
- verify(mTestDeps, times(2))
- .newNetworkAgent(any(), any(), anyString(), any(), lpCaptor.capture(), any(), any(),
- any(), any());
- verify(mMockNetworkAgent).unregister();
- // mMockNetworkAgent is an old NetworkAgent, so it won't update LinkProperties after
- // unregistering.
- verify(mMockNetworkAgent, never()).doSendLinkProperties(any());
-
- final LinkProperties lp = lpCaptor.getValue();
-
- for (LinkAddress addr : lp.getLinkAddresses()) {
- if (addr.isIpv6()) {
- fail("IPv6 address found on VPN with MTU < IPv6 minimum MTU");
- }
- }
-
- for (InetAddress dnsAddr : lp.getDnsServers()) {
- if (dnsAddr instanceof Inet6Address) {
- fail("IPv6 DNS server found on VPN with MTU < IPv6 minimum MTU");
- }
- }
-
- for (RouteInfo routeInfo : lp.getRoutes()) {
- if (routeInfo.getDestinationLinkAddress().isIpv6()
- && !routeInfo.isIPv6UnreachableDefault()) {
- fail("IPv6 route found on VPN with MTU < IPv6 minimum MTU");
- }
- }
-
- assertEquals(newMtu, lp.getMtu());
-
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- }
-
- @Test
- public void testStartPlatformVpnReestablishes_mobikeDisabled() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
-
- // Forget the first IKE creation to be prepared to capture callbacks of the second
- // IKE session
- resetIkev2SessionCreator(mock(Vpn.IkeSessionWrapper.class));
-
- // Mock network switch
- vpnSnapShot.nwCb.onLost(TEST_NETWORK);
- vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
- // The old IKE Session will not be killed until receiving network capabilities change.
- verify(mIkeSessionWrapper, never()).kill();
-
- vpnSnapShot.nwCb.onCapabilitiesChanged(
- TEST_NETWORK_2, new NetworkCapabilities.Builder().build());
- // Verify the old IKE Session is killed
- verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).kill();
-
- // Capture callbacks of the new IKE Session
- final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
- verifyCreateIkeAndCaptureCbs();
- final IkeSessionCallback ikeCb = cbPair.first;
- final ChildSessionCallback childCb = cbPair.second;
-
- // Mock the IKE Session setup
- ikeCb.onOpened(createIkeConfig(createIkeConnectInfo_2(), false /* isMobikeEnabled */));
-
- childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
- childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
- childCb.onOpened(createChildConfig());
-
- // Expect 2 times since there have been two Session setups
- verifyApplyTunnelModeTransforms(2);
-
- // Verify mNetworkCapabilities and mNetworkAgent are updated
- assertEquals(
- Collections.singletonList(TEST_NETWORK_2),
- vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
- verify(mMockNetworkAgent)
- .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
-
- vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
- }
-
- private String getDump(@NonNull final Vpn vpn) {
- final StringWriter sw = new StringWriter();
- final IndentingPrintWriter writer = new IndentingPrintWriter(sw, "");
- vpn.dump(writer);
- writer.flush();
- return sw.toString();
- }
-
- private int countMatches(@NonNull final Pattern regexp, @NonNull final String string) {
- final Matcher m = regexp.matcher(string);
- int i = 0;
- while (m.find()) ++i;
- return i;
- }
-
- @Test
- public void testNCEventChanges() throws Exception {
- final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder()
- .addTransportType(TRANSPORT_CELLULAR)
- .addCapability(NET_CAPABILITY_INTERNET)
- .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
- .setLinkDownstreamBandwidthKbps(1000)
- .setLinkUpstreamBandwidthKbps(500);
-
- final Ikev2VpnProfile ikeProfile =
- new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
- .setAuthPsk(TEST_VPN_PSK)
- .setBypassable(true /* isBypassable */)
- .setAutomaticNattKeepaliveTimerEnabled(true)
- .setAutomaticIpVersionSelectionEnabled(true)
- .build();
-
- final PlatformVpnSnapshot vpnSnapShot =
- verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
- ncBuilder.build(), false /* mtuSupportsIpv6 */,
- true /* areLongLivedTcpConnectionsExpensive */);
-
- // Calls to onCapabilitiesChanged will be thrown to the executor for execution ; by
- // default this will incur a 10ms delay before it's executed, messing with the timing
- // of the log and having the checks for counts in equals() below flake.
- mExecutor.executeDirect = true;
-
- // First nc changed triggered by verifySetupPlatformVpn
- final Pattern pattern = Pattern.compile("Cap changed from", Pattern.MULTILINE);
- final String stage1 = getDump(vpnSnapShot.vpn);
- assertEquals(1, countMatches(pattern, stage1));
-
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
- final String stage2 = getDump(vpnSnapShot.vpn);
- // Was the same caps, there should still be only 1 match
- assertEquals(1, countMatches(pattern, stage2));
-
- ncBuilder.setLinkDownstreamBandwidthKbps(1200)
- .setLinkUpstreamBandwidthKbps(300);
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
- final String stage3 = getDump(vpnSnapShot.vpn);
- // Was not an important change, should not be logged, still only 1 match
- assertEquals(1, countMatches(pattern, stage3));
-
- ncBuilder.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
- final String stage4 = getDump(vpnSnapShot.vpn);
- // Change to caps is important, should cause a new match
- assertEquals(2, countMatches(pattern, stage4));
-
- ncBuilder.removeCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
- ncBuilder.setLinkDownstreamBandwidthKbps(600);
- vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
- final String stage5 = getDump(vpnSnapShot.vpn);
- // Change to caps is important, should cause a new match even with the unimportant change
- assertEquals(3, countMatches(pattern, stage5));
- }
- // TODO : beef up event logs tests
-
- private void verifyHandlingNetworkLoss(PlatformVpnSnapshot vpnSnapShot) throws Exception {
- // Forget the #sendLinkProperties during first setup.
- reset(mMockNetworkAgent);
-
- // Mock network loss
- vpnSnapShot.nwCb.onLost(TEST_NETWORK);
-
- // Mock the grace period expires
- verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
-
- final ArgumentCaptor<LinkProperties> lpCaptor =
- ArgumentCaptor.forClass(LinkProperties.class);
- verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS))
- .doSendLinkProperties(lpCaptor.capture());
- final LinkProperties lp = lpCaptor.getValue();
-
- assertNull(lp.getInterfaceName());
- final List<RouteInfo> expectedRoutes = Arrays.asList(
- new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null /* gateway */,
- null /* iface */, RTN_UNREACHABLE),
- new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null /* gateway */,
- null /* iface */, RTN_UNREACHABLE));
- assertEquals(expectedRoutes, lp.getRoutes());
-
- verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS)).unregister();
- }
-
- @Test
- public void testStartPlatformVpnHandlesNetworkLoss_mobikeEnabled() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
- verifyHandlingNetworkLoss(vpnSnapShot);
- }
-
- @Test
- public void testStartPlatformVpnHandlesNetworkLoss_mobikeDisabled() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
- verifyHandlingNetworkLoss(vpnSnapShot);
- }
-
- private ConnectivityDiagnosticsCallback getConnectivityDiagCallback() {
- final ArgumentCaptor<ConnectivityDiagnosticsCallback> cdcCaptor =
- ArgumentCaptor.forClass(ConnectivityDiagnosticsCallback.class);
- verify(mCdm).registerConnectivityDiagnosticsCallback(
- any(), any(), cdcCaptor.capture());
- return cdcCaptor.getValue();
- }
-
- private DataStallReport createDataStallReport() {
- return new DataStallReport(TEST_NETWORK, 1234 /* reportTimestamp */,
- 1 /* detectionMethod */, new LinkProperties(), new NetworkCapabilities(),
- new PersistableBundle());
- }
-
- private void verifyMobikeTriggered(List<Network> expected, int retryIndex) {
- // Verify retry is scheduled
- final long expectedDelayMs = mTestDeps.getValidationFailRecoveryMs(retryIndex);
- final ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class);
- verify(mExecutor, times(retryIndex + 1)).schedule(
- any(Runnable.class), delayCaptor.capture(), eq(TimeUnit.MILLISECONDS));
- final List<Long> delays = delayCaptor.getAllValues();
- assertEquals(expectedDelayMs, (long) delays.get(delays.size() - 1));
-
- final ArgumentCaptor<Network> networkCaptor = ArgumentCaptor.forClass(Network.class);
- verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS + expectedDelayMs))
- .setNetwork(networkCaptor.capture(), anyInt() /* ipVersion */,
- anyInt() /* encapType */, anyInt() /* keepaliveDelay */);
- assertEquals(expected, Collections.singletonList(networkCaptor.getValue()));
- }
-
- @Test
- public void testDataStallInIkev2VpnMobikeDisabled() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
-
- doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-
- // Should not trigger MOBIKE if MOBIKE is not enabled
- verify(mIkeSessionWrapper, never()).setNetwork(any() /* network */,
- anyInt() /* ipVersion */, anyInt() /* encapType */, anyInt() /* keepaliveDelay */);
- }
-
- @Test
- public void testDataStallInIkev2VpnRecoveredByMobike() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
- doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_NOT_VALID);
- // Verify MOBIKE is triggered
- verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
- 0 /* retryIndex */);
- // Validation failure on VPN network should trigger a re-evaluation request for the
- // underlying network.
- verify(mConnectivityManager).reportNetworkConnectivity(TEST_NETWORK, false);
-
- reset(mIkev2SessionCreator);
- reset(mExecutor);
-
- // Send validation status update.
- // Recovered and get network validated. It should not trigger the ike session reset.
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_VALID);
- // Verify that the retry count is reset. The mValidationFailRetryCount will not be reset
- // until the executor finishes the execute() call, so wait until the all tasks are executed.
- waitForIdleSerialExecutor(mExecutor, TEST_TIMEOUT_MS);
- assertEquals(0,
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).mValidationFailRetryCount);
- verify(mIkev2SessionCreator, never()).createIkeSession(
- any(), any(), any(), any(), any(), any());
-
- reset(mIkeSessionWrapper);
- reset(mExecutor);
-
- // Another validation fail should trigger another reportNetworkConnectivity
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_NOT_VALID);
- verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
- 0 /* retryIndex */);
- verify(mConnectivityManager, times(2)).reportNetworkConnectivity(TEST_NETWORK, false);
- }
-
- @Test
- public void testDataStallInIkev2VpnNotRecoveredByMobike() throws Exception {
- final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
- createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
- int retry = 0;
- doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_NOT_VALID);
- verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
- retry++);
- // Validation failure on VPN network should trigger a re-evaluation request for the
- // underlying network.
- verify(mConnectivityManager).reportNetworkConnectivity(TEST_NETWORK, false);
- reset(mIkev2SessionCreator);
-
- // Second validation status update.
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_NOT_VALID);
- verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
- retry++);
- // Call to reportNetworkConnectivity should only happen once. No further interaction.
- verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false);
-
- // Use real delay to verify reset session will not be performed if there is an existing
- // recovery for resetting the session.
- mExecutor.delayMs = TestExecutor.REAL_DELAY;
- mExecutor.executeDirect = true;
- // Send validation status update should result in ike session reset.
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-
- // Verify session reset is scheduled
- long expectedDelay = mTestDeps.getValidationFailRecoveryMs(retry++);
- final ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class);
- verify(mExecutor, times(retry)).schedule(any(Runnable.class), delayCaptor.capture(),
- eq(TimeUnit.MILLISECONDS));
- final List<Long> delays = delayCaptor.getAllValues();
- assertEquals(expectedDelay, (long) delays.get(delays.size() - 1));
- // Call to reportNetworkConnectivity should only happen once. No further interaction.
- verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false);
-
- // Another invalid status reported should not trigger other scheduled recovery.
- expectedDelay = mTestDeps.getValidationFailRecoveryMs(retry++);
- ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
- NetworkAgent.VALIDATION_STATUS_NOT_VALID);
- verify(mExecutor, never()).schedule(
- any(Runnable.class), eq(expectedDelay), eq(TimeUnit.MILLISECONDS));
-
- // Verify that session being reset
- verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS + expectedDelay))
- .createIkeSession(any(), any(), any(), any(), any(), any());
- // Call to reportNetworkConnectivity should only happen once. No further interaction.
- verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false);
- }
-
- @Test
- public void testStartLegacyVpnType() throws Exception {
- setMockedUsers(PRIMARY_USER);
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final VpnProfile profile = new VpnProfile("testProfile" /* key */);
-
- profile.type = VpnProfile.TYPE_PPTP;
- assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
- profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK;
- assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
- }
-
- @Test
- public void testStartLegacyVpnModifyProfile_TypePSK() throws Exception {
- setMockedUsers(PRIMARY_USER);
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final Ikev2VpnProfile ikev2VpnProfile =
- new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
- .setAuthPsk(TEST_VPN_PSK)
- .build();
- final VpnProfile profile = ikev2VpnProfile.toVpnProfile();
-
- startLegacyVpn(vpn, profile);
- assertEquals(profile, ikev2VpnProfile.toVpnProfile());
- }
-
- private void assertTransportInfoMatches(NetworkCapabilities nc, int type) {
- assertNotNull(nc);
- VpnTransportInfo ti = (VpnTransportInfo) nc.getTransportInfo();
- assertNotNull(ti);
- assertEquals(type, ti.getType());
- }
-
- // Make it public and un-final so as to spy it
- public class TestDeps extends Vpn.Dependencies {
- TestDeps() {}
-
- @Override
- public boolean isCallerSystem() {
- return true;
- }
-
- @Override
- public PendingIntent getIntentForStatusPanel(Context context) {
- return null;
- }
-
- @Override
- public ParcelFileDescriptor adoptFd(Vpn vpn, int mtu) {
- return new ParcelFileDescriptor(new FileDescriptor());
- }
-
- @Override
- public int jniCreate(Vpn vpn, int mtu) {
- // Pick a random positive number as fd to return.
- return 345;
- }
-
- @Override
- public String jniGetName(Vpn vpn, int fd) {
- return TEST_IFACE_NAME;
- }
-
- @Override
- public int jniSetAddresses(Vpn vpn, String interfaze, String addresses) {
- if (addresses == null) return 0;
- // Return the number of addresses.
- return addresses.split(" ").length;
- }
-
- @Override
- public void setBlocking(FileDescriptor fd, boolean blocking) {}
-
- @Override
- public DeviceIdleInternal getDeviceIdleInternal() {
- return mDeviceIdleInternal;
- }
-
- @Override
- public long getValidationFailRecoveryMs(int retryCount) {
- // Simply return retryCount as the delay seconds for retrying.
- return retryCount * 100L;
- }
-
- @Override
- public ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor() {
- return mExecutor;
- }
-
- public boolean mIgnoreCallingUidChecks = true;
- @Override
- public void verifyCallingUidAndPackage(Context context, String packageName, int userId) {
- if (!mIgnoreCallingUidChecks) {
- super.verifyCallingUidAndPackage(context, packageName, userId);
- }
- }
- }
-
- /**
- * Mock some methods of vpn object.
- */
- private Vpn createVpn(@UserIdInt int userId) {
- final Context asUserContext = mock(Context.class, AdditionalAnswers.delegatesTo(mContext));
- doReturn(UserHandle.of(userId)).when(asUserContext).getUser();
- when(mContext.createContextAsUser(eq(UserHandle.of(userId)), anyInt()))
- .thenReturn(asUserContext);
- final TestLooper testLooper = new TestLooper();
- final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, mTestDeps, mNetService,
- mNetd, userId, mVpnProfileStore, mSystemServices, mIkev2SessionCreator);
- verify(mConnectivityManager, times(1)).registerNetworkProvider(argThat(
- provider -> provider.getName().contains("VpnNetworkProvider")
- ));
- return vpn;
- }
-
- /**
- * Populate {@link #mUserManager} with a list of fake users.
- */
- private void setMockedUsers(UserInfo... users) {
- final Map<Integer, UserInfo> userMap = new ArrayMap<>();
- for (UserInfo user : users) {
- userMap.put(user.id, user);
- }
-
- /**
- * @see UserManagerService#getUsers(boolean)
- */
- doAnswer(invocation -> {
- final ArrayList<UserInfo> result = new ArrayList<>(users.length);
- for (UserInfo ui : users) {
- if (ui.isEnabled() && !ui.partial) {
- result.add(ui);
- }
- }
- return result;
- }).when(mUserManager).getAliveUsers();
-
- doAnswer(invocation -> {
- final int id = (int) invocation.getArguments()[0];
- return userMap.get(id);
- }).when(mUserManager).getUserInfo(anyInt());
- }
-
- /**
- * Populate {@link #mPackageManager} with a fake packageName-to-UID mapping.
- */
- private void setMockedPackages(final Map<String, Integer> packages) {
- try {
- doAnswer(invocation -> {
- final String appName = (String) invocation.getArguments()[0];
- final int userId = (int) invocation.getArguments()[1];
- Integer appId = packages.get(appName);
- if (appId == null) throw new PackageManager.NameNotFoundException(appName);
- return UserHandle.getUid(userId, appId);
- }).when(mPackageManager).getPackageUidAsUser(anyString(), anyInt());
- } catch (Exception e) {
- }
- }
-}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index 121f844..df48f6c 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -16,6 +16,8 @@
package com.android.server.connectivity.mdns
+import android.content.Context
+import android.content.res.Resources
import android.net.InetAddresses.parseNumericAddress
import android.net.LinkAddress
import android.net.Network
@@ -26,13 +28,17 @@
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
+import com.android.connectivity.resources.R
import com.android.net.module.util.SharedLog
+import com.android.server.connectivity.ConnectivityResources
import com.android.server.connectivity.mdns.MdnsAdvertiser.AdvertiserCallback
+import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE
import com.android.server.connectivity.mdns.MdnsSocketProvider.SocketCallback
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.waitForIdle
import java.net.NetworkInterface
+import java.time.Duration
import java.util.Objects
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
@@ -55,9 +61,10 @@
private const val SERVICE_ID_1 = 1
private const val SERVICE_ID_2 = 2
-private const val LONG_SERVICE_ID_1 = 3
-private const val LONG_SERVICE_ID_2 = 4
-private const val CASE_INSENSITIVE_TEST_SERVICE_ID = 5
+private const val SERVICE_ID_3 = 3
+private const val LONG_SERVICE_ID_1 = 4
+private const val LONG_SERVICE_ID_2 = 5
+private const val CASE_INSENSITIVE_TEST_SERVICE_ID = 6
private const val TIMEOUT_MS = 10_000L
private val TEST_ADDR = parseNumericAddress("2001:db8::123")
private val TEST_ADDR2 = parseNumericAddress("2001:db8::124")
@@ -71,6 +78,7 @@
private const val TEST_SUBTYPE2 = "_subtype2"
private val TEST_INTERFACE1 = "test_iface1"
private val TEST_INTERFACE2 = "test_iface2"
+private val TEST_CLIENT_UID_1 = 10010
private val TEST_OFFLOAD_PACKET1 = byteArrayOf(0x01, 0x02, 0x03)
private val TEST_OFFLOAD_PACKET2 = byteArrayOf(0x02, 0x03, 0x04)
private val DEFAULT_ADVERTISING_OPTION = MdnsAdvertisingOptions.getDefaultOptions()
@@ -150,6 +158,12 @@
OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
)
+private val SERVICES_PRIORITY_LIST = arrayOf(
+ "0:_advertisertest._tcp",
+ "5:_prioritytest._udp",
+ "5:_otherprioritytest._tcp"
+)
+
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsAdvertiserTest {
@@ -164,6 +178,8 @@
private val mockInterfaceAdvertiser1 = mock(MdnsInterfaceAdvertiser::class.java)
private val mockInterfaceAdvertiser2 = mock(MdnsInterfaceAdvertiser::class.java)
private val mockDeps = mock(MdnsAdvertiser.Dependencies::class.java)
+ private val context = mock(Context::class.java)
+ private val resources = mock(Resources::class.java)
private val flags = MdnsFeatureFlags.newBuilder().setIsMdnsOffloadFeatureEnabled(true).build()
@Before
@@ -184,12 +200,21 @@
doReturn(TEST_INTERFACE2).`when`(mockInterfaceAdvertiser2).socketInterfaceName
doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
SERVICE_ID_1)
+ doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
+ SERVICE_ID_2)
+ doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser1).getRawOffloadPayload(
+ SERVICE_ID_3)
doReturn(TEST_OFFLOAD_PACKET1).`when`(mockInterfaceAdvertiser2).getRawOffloadPayload(
SERVICE_ID_1)
+ doReturn(resources).`when`(context).getResources()
+ doReturn(SERVICES_PRIORITY_LIST).`when`(resources).getStringArray(
+ R.array.config_nsdOffloadServicesPriority)
+ ConnectivityResources.setResourcesContextForTest(context)
}
@After
fun tearDown() {
+ ConnectivityResources.setResourcesContextForTest(null)
thread.quitSafely()
thread.join()
}
@@ -203,9 +228,9 @@
@Test
fun testAddService_OneNetwork() {
val advertiser =
- MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
@@ -232,7 +257,10 @@
verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE))
// Service is conflicted.
- postSync { intAdvCbCaptor.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
+ postSync {
+ intAdvCbCaptor.value
+ .onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1, CONFLICT_SERVICE)
+ }
// Verify the metrics data
doReturn(25).`when`(mockInterfaceAdvertiser1).getServiceRepliedRequestsCount(SERVICE_ID_1)
@@ -258,16 +286,15 @@
postSync { socketCb.onInterfaceDestroyed(TEST_SOCKETKEY_1, mockSocket1) }
verify(mockInterfaceAdvertiser1).destroyNow()
- postSync { intAdvCbCaptor.value.onDestroyed(mockSocket1) }
verify(cb).onOffloadStop(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE2))
}
@Test
fun testAddService_AllNetworksWithSubType() {
val advertiser =
- MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
- DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE_SUBTYPE.network),
@@ -286,9 +313,9 @@
eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(
- anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE))
+ anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
verify(mockInterfaceAdvertiser2).addService(
- anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE))
+ anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor1.value.onServiceProbingSucceeded(
@@ -305,9 +332,18 @@
argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
// Services are conflicted.
- postSync { intAdvCbCaptor1.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
- postSync { intAdvCbCaptor1.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
- postSync { intAdvCbCaptor2.value.onServiceConflict(mockInterfaceAdvertiser2, SERVICE_ID_1) }
+ postSync {
+ intAdvCbCaptor1.value
+ .onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1, CONFLICT_SERVICE)
+ }
+ postSync {
+ intAdvCbCaptor1.value
+ .onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1, CONFLICT_SERVICE)
+ }
+ postSync {
+ intAdvCbCaptor2.value
+ .onServiceConflict(mockInterfaceAdvertiser2, SERVICE_ID_1, CONFLICT_SERVICE)
+ }
// Verify the metrics data
doReturn(10).`when`(mockInterfaceAdvertiser1).getServiceRepliedRequestsCount(SERVICE_ID_1)
@@ -327,19 +363,78 @@
verify(cb).onOffloadStop(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO))
verify(cb).onOffloadStop(eq(TEST_INTERFACE2), eq(OFFLOAD_SERVICEINFO))
- // Interface advertisers call onDestroyed after sending exit announcements
- postSync { intAdvCbCaptor1.value.onDestroyed(mockSocket1) }
+ // Interface advertisers call onAllServicesRemoved after sending exit announcements
+ postSync { intAdvCbCaptor1.value.onAllServicesRemoved(mockSocket1) }
verify(socketProvider, never()).unrequestSocket(any())
- postSync { intAdvCbCaptor2.value.onDestroyed(mockSocket2) }
+ postSync { intAdvCbCaptor2.value.onAllServicesRemoved(mockSocket2) }
verify(socketProvider).unrequestSocket(socketCb)
}
@Test
+ fun testAddService_OffloadPriority() {
+ val advertiser =
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+ postSync {
+ advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1, DEFAULT_ADVERTISING_OPTION,
+ TEST_CLIENT_UID_1)
+ advertiser.addOrUpdateService(SERVICE_ID_2,
+ NsdServiceInfo("TestService2", "_PRIORITYTEST._udp").apply {
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ }, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1)
+ advertiser.addOrUpdateService(
+ SERVICE_ID_3,
+ NsdServiceInfo("TestService3", "_notprioritized._tcp").apply {
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ }, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1)
+ }
+
+ val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+ verify(socketProvider).requestSocket(eq(SERVICE_1.network), socketCbCaptor.capture())
+
+ val socketCb = socketCbCaptor.value
+ postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+
+ val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
+ verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any(), any()
+ )
+
+ doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
+ doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_2)
+ doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_3)
+ postSync {
+ intAdvCbCaptor1.value.onServiceProbingSucceeded(mockInterfaceAdvertiser1, SERVICE_ID_1)
+ intAdvCbCaptor1.value.onServiceProbingSucceeded(mockInterfaceAdvertiser1, SERVICE_ID_2)
+ intAdvCbCaptor1.value.onServiceProbingSucceeded(mockInterfaceAdvertiser1, SERVICE_ID_3)
+ }
+
+ verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE))
+ verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OffloadServiceInfo(
+ OffloadServiceInfo.Key("TestService2", "_PRIORITYTEST._udp"),
+ emptyList() /* subtypes */,
+ "Android_test.local",
+ TEST_OFFLOAD_PACKET1,
+ 5, /* priority */
+ OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+ )))
+ verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE1), eq(OffloadServiceInfo(
+ OffloadServiceInfo.Key("TestService3", "_notprioritized._tcp"),
+ emptyList() /* subtypes */,
+ "Android_test.local",
+ TEST_OFFLOAD_PACKET1,
+ Integer.MAX_VALUE, /* priority */
+ OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+ )))
+ }
+
+ @Test
fun testAddService_Conflicts() {
val advertiser =
- MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
val oneNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), oneNetSocketCbCaptor.capture())
@@ -347,18 +442,18 @@
// Register a service with the same name on all networks (name conflict)
postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE,
- DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
val allNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), allNetSocketCbCaptor.capture())
val allNetSocketCb = allNetSocketCbCaptor.value
postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_1, LONG_SERVICE_1,
- DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
- DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
postSync { advertiser.addOrUpdateService(CASE_INSENSITIVE_TEST_SERVICE_ID,
- ALL_NETWORKS_SERVICE_2, DEFAULT_ADVERTISING_OPTION) }
+ ALL_NETWORKS_SERVICE_2, DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
// Callbacks for matching network and all networks both get the socket
postSync {
@@ -394,15 +489,15 @@
eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(SERVICE_1) })
+ argThat { it.matches(SERVICE_1) }, any())
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
- argThat { it.matches(expectedRenamed) })
+ argThat { it.matches(expectedRenamed) }, any())
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
- argThat { it.matches(LONG_SERVICE_1) })
+ argThat { it.matches(LONG_SERVICE_1) }, any())
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
- argThat { it.matches(expectedLongRenamed) })
+ argThat { it.matches(expectedLongRenamed) }, any())
verify(mockInterfaceAdvertiser1).addService(eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
- argThat { it.matches(expectedCaseInsensitiveRenamed) })
+ argThat { it.matches(expectedCaseInsensitiveRenamed) }, any())
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
@@ -425,9 +520,10 @@
@Test
fun testAddOrUpdateService_Updates() {
val advertiser =
- MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags,
+ context)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
- DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
@@ -436,37 +532,55 @@
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE) })
+ argThat { it.matches(ALL_NETWORKS_SERVICE) }, any())
val updateOptions = MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(true).build()
// Update with serviceId that is not registered yet should fail
postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE_SUBTYPE,
- updateOptions) }
+ updateOptions, TEST_CLIENT_UID_1) }
verify(cb).onRegisterServiceFailed(SERVICE_ID_2, NsdManager.FAILURE_INTERNAL_ERROR)
// Update service with different NsdServiceInfo should fail
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1_SUBTYPE, updateOptions) }
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1_SUBTYPE, updateOptions,
+ TEST_CLIENT_UID_1) }
verify(cb).onRegisterServiceFailed(SERVICE_ID_1, NsdManager.FAILURE_INTERNAL_ERROR)
// Update service with same NsdServiceInfo but different subType should succeed
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
- updateOptions) }
+ updateOptions, TEST_CLIENT_UID_1) }
verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(setOf(TEST_SUBTYPE)))
// Newly created MdnsInterfaceAdvertiser will get addService() call.
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_2, mockSocket2, listOf(TEST_LINKADDR2)) }
verify(mockInterfaceAdvertiser2).addService(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
+ argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) }, any())
+ }
+
+ @Test
+ fun testAddOrUpdateService_customTtl_registeredSuccess() {
+ val advertiser = MdnsAdvertiser(
+ thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+ val updateOptions =
+ MdnsAdvertisingOptions.newBuilder().setTtl(Duration.ofSeconds(30)).build()
+
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
+ updateOptions, TEST_CLIENT_UID_1) }
+
+ val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+ verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
+ val socketCb = socketCbCaptor.value
+ postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+ verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1), any(), eq(updateOptions))
}
@Test
fun testRemoveService_whenAllServiceRemoved_thenUpdateHostName() {
val advertiser =
- MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
verify(mockDeps, times(1)).generateHostname()
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
postSync { advertiser.removeService(SERVICE_ID_1) }
verify(mockDeps, times(2)).generateHostname()
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
index 2797462..27242f1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
@@ -55,6 +55,7 @@
private val socket = mock(MdnsInterfaceSocket::class.java)
private val sharedLog = mock(SharedLog::class.java)
private val buffer = ByteArray(1500)
+ private val flags = MdnsFeatureFlags.newBuilder().build()
@Before
fun setUp() {
@@ -83,7 +84,7 @@
@Test
fun testAnnounce() {
val replySender = MdnsReplySender(
- thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */, flags)
@Suppress("UNCHECKED_CAST")
val cb = mock(MdnsPacketRepeater.PacketRepeaterCallback::class.java)
as MdnsPacketRepeater.PacketRepeaterCallback<BaseAnnouncementInfo>
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
index 5251e2a..b5c0132 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -18,6 +18,8 @@
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
@@ -65,8 +67,9 @@
private static final String SERVICE_TYPE_2 = "_test._tcp.local";
private static final Network NETWORK_1 = Mockito.mock(Network.class);
private static final Network NETWORK_2 = Mockito.mock(Network.class);
+ private static final int INTERFACE_INDEX_NULL_NETWORK = 123;
private static final SocketKey SOCKET_KEY_NULL_NETWORK =
- new SocketKey(null /* network */, 999 /* interfaceIndex */);
+ new SocketKey(null /* network */, INTERFACE_INDEX_NULL_NETWORK);
private static final SocketKey SOCKET_KEY_NETWORK_1 =
new SocketKey(NETWORK_1, 998 /* interfaceIndex */);
private static final SocketKey SOCKET_KEY_NETWORK_2 =
@@ -97,6 +100,8 @@
private HandlerThread thread;
private Handler handler;
+ private int createdServiceTypeClientCount;
+
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -106,11 +111,13 @@
handler = new Handler(thread.getLooper());
doReturn(thread.getLooper()).when(socketClient).getLooper();
doReturn(true).when(socketClient).supportsRequestingSpecificNetworks();
+ createdServiceTypeClientCount = 0;
discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient,
sharedLog, MdnsFeatureFlags.newBuilder().build()) {
@Override
MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
@NonNull SocketKey socketKey) {
+ createdServiceTypeClientCount++;
final Pair<String, SocketKey> perSocketServiceType =
Pair.create(serviceType, socketKey);
if (perSocketServiceType.equals(PER_SOCKET_SERVICE_TYPE_1_NULL_NETWORK)) {
@@ -128,6 +135,7 @@
PER_SOCKET_SERVICE_TYPE_2_NETWORK_2)) {
return mockServiceTypeClientType2Network2;
}
+ fail("Unexpected perSocketServiceType: " + perSocketServiceType);
return null;
}
};
@@ -324,7 +332,6 @@
// Receive a response, it should be processed on the client.
final MdnsPacket response = createMdnsPacket(SERVICE_TYPE_1);
- final int ifIndex = 1;
runOnHandler(() -> discoveryManager.onResponseReceived(response, SOCKET_KEY_NULL_NETWORK));
verify(mockServiceTypeClientType1NullNetwork).processResponse(
response, SOCKET_KEY_NULL_NETWORK);
@@ -350,6 +357,39 @@
verify(socketClient, never()).stopDiscovery();
}
+ @Test
+ public void testInterfaceIndexRequested_OnlyUsesSelectedInterface() throws IOException {
+ final MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder()
+ .setNetwork(null /* network */)
+ .setInterfaceIndex(INTERFACE_INDEX_NULL_NETWORK)
+ .build();
+
+ final SocketCreationCallback callback = expectSocketCreationCallback(
+ SERVICE_TYPE_1, mockListenerOne, searchOptions);
+ final SocketKey unusedIfaceKey = new SocketKey(null, INTERFACE_INDEX_NULL_NETWORK + 1);
+ final SocketKey matchingIfaceWithNetworkKey =
+ new SocketKey(Mockito.mock(Network.class), INTERFACE_INDEX_NULL_NETWORK);
+ runOnHandler(() -> {
+ callback.onSocketCreated(unusedIfaceKey);
+ callback.onSocketCreated(matchingIfaceWithNetworkKey);
+ callback.onSocketCreated(SOCKET_KEY_NULL_NETWORK);
+ callback.onSocketCreated(SOCKET_KEY_NETWORK_1);
+ });
+ // Only the client for INTERFACE_INDEX_NULL_NETWORK is created
+ verify(mockServiceTypeClientType1NullNetwork).startSendAndReceive(
+ mockListenerOne, searchOptions);
+ assertEquals(1, createdServiceTypeClientCount);
+
+ runOnHandler(() -> {
+ callback.onSocketDestroyed(SOCKET_KEY_NETWORK_1);
+ callback.onSocketDestroyed(SOCKET_KEY_NULL_NETWORK);
+ callback.onSocketDestroyed(matchingIfaceWithNetworkKey);
+ callback.onSocketDestroyed(unusedIfaceKey);
+ });
+ verify(mockServiceTypeClientType1NullNetwork).notifySocketDestroyed();
+ }
+
private MdnsPacket createMdnsPacket(String serviceType) {
final String[] type = TextUtils.split(serviceType, "\\.");
final ArrayList<String> name = new ArrayList<>(type.length + 1);
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index 0c04bff..629ac67 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -26,6 +26,7 @@
import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo
import com.android.server.connectivity.mdns.MdnsAnnouncer.BaseAnnouncementInfo
import com.android.server.connectivity.mdns.MdnsAnnouncer.ExitAnnouncementInfo
+import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE
import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.EXIT_ANNOUNCEMENT_DELAY_MS
import com.android.server.connectivity.mdns.MdnsPacketRepeater.PacketRepeaterCallback
import com.android.server.connectivity.mdns.MdnsProber.ProbingInfo
@@ -35,6 +36,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
@@ -44,6 +46,7 @@
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyString
+import org.mockito.Mockito.argThat
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.eq
@@ -51,6 +54,7 @@
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
+import org.mockito.Mockito.inOrder
private const val LOG_TAG = "testlogtag"
private const val TIMEOUT_MS = 10_000L
@@ -61,6 +65,7 @@
private const val TEST_SERVICE_ID_1 = 42
private const val TEST_SERVICE_ID_DUPLICATE = 43
+private const val TEST_SERVICE_ID_2 = 44
private val TEST_SERVICE_1 = NsdServiceInfo().apply {
serviceType = "_testservice._tcp"
serviceName = "MyTestService"
@@ -74,6 +79,13 @@
port = 12345
}
+private val TEST_SERVICE_1_CUSTOM_HOST = NsdServiceInfo().apply {
+ serviceType = "_testservice._tcp"
+ serviceName = "MyTestService"
+ hostname = "MyTestHost"
+ port = 12345
+}
+
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsInterfaceAdvertiserTest {
@@ -86,7 +98,8 @@
private val announcer = mock(MdnsAnnouncer::class.java)
private val prober = mock(MdnsProber::class.java)
private val sharedlog = SharedLog("MdnsInterfaceAdvertiserTest")
- private val flags = MdnsFeatureFlags.newBuilder().build()
+ private val flags = MdnsFeatureFlags.newBuilder()
+ .setIsKnownAnswerSuppressionEnabled(true).build()
@Suppress("UNCHECKED_CAST")
private val probeCbCaptor = ArgumentCaptor.forClass(PacketRepeaterCallback::class.java)
as ArgumentCaptor<PacketRepeaterCallback<ProbingInfo>>
@@ -117,7 +130,8 @@
@Before
fun setUp() {
doReturn(repository).`when`(deps).makeRecordRepository(any(), eq(TEST_HOSTNAME), any())
- doReturn(replySender).`when`(deps).makeReplySender(anyString(), any(), any(), any(), any())
+ doReturn(replySender).`when`(deps).makeReplySender(
+ anyString(), any(), any(), any(), any(), any())
doReturn(announcer).`when`(deps).makeMdnsAnnouncer(anyString(), any(), any(), any(), any())
doReturn(prober).`when`(deps).makeMdnsProber(anyString(), any(), any(), any(), any())
@@ -126,7 +140,7 @@
knownServices.add(inv.getArgument(0))
-1
- }.`when`(repository).addService(anyInt(), any())
+ }.`when`(repository).addService(anyInt(), any(), any())
doAnswer { inv ->
knownServices.remove(inv.getArgument(0))
null
@@ -173,7 +187,94 @@
// Exit announcements finish: the advertiser has no left service and destroys itself
announceCb.onFinished(testExitInfo)
thread.waitForIdle(TIMEOUT_MS)
- verify(cb).onDestroyed(socket)
+ verify(cb).onAllServicesRemoved(socket)
+ }
+
+ @Test
+ fun testAddRemoveServiceWithCustomHost_restartProbingForProbingServices() {
+ val customHost1 = NsdServiceInfo().apply {
+ hostname = "MyTestHost"
+ hostAddresses = listOf(
+ parseNumericAddress("192.0.2.23"),
+ parseNumericAddress("2001:db8::1"))
+ }
+ addServiceAndFinishProbing(TEST_SERVICE_ID_1, customHost1)
+ addServiceAndFinishProbing(TEST_SERVICE_ID_2, TEST_SERVICE_1_CUSTOM_HOST)
+ repository.setServiceProbing(TEST_SERVICE_ID_2)
+ val probingInfo = mock(ProbingInfo::class.java)
+ doReturn("MyTestHost")
+ .`when`(repository).getHostnameForServiceId(TEST_SERVICE_ID_1)
+ doReturn(TEST_SERVICE_ID_2).`when`(probingInfo).serviceId
+ doReturn(listOf(probingInfo))
+ .`when`(repository).restartProbingForHostname("MyTestHost")
+ val inOrder = inOrder(prober, announcer)
+
+ // Remove the custom host: the custom host's announcement is stopped and the probing
+ // services which use that hostname are re-announced.
+ advertiser.removeService(TEST_SERVICE_ID_1)
+
+ inOrder.verify(prober).stop(TEST_SERVICE_ID_1)
+ inOrder.verify(announcer).stop(TEST_SERVICE_ID_1)
+ inOrder.verify(prober).stop(TEST_SERVICE_ID_2)
+ inOrder.verify(prober).startProbing(probingInfo)
+ }
+
+ @Test
+ fun testAddRemoveServiceWithCustomHost_restartAnnouncingForProbedServices() {
+ val customHost1 = NsdServiceInfo().apply {
+ hostname = "MyTestHost"
+ hostAddresses = listOf(
+ parseNumericAddress("192.0.2.23"),
+ parseNumericAddress("2001:db8::1"))
+ }
+ addServiceAndFinishProbing(TEST_SERVICE_ID_1, customHost1)
+ val announcementInfo =
+ addServiceAndFinishProbing(TEST_SERVICE_ID_2, TEST_SERVICE_1_CUSTOM_HOST)
+ doReturn("MyTestHost")
+ .`when`(repository).getHostnameForServiceId(TEST_SERVICE_ID_1)
+ doReturn(listOf(announcementInfo))
+ .`when`(repository).restartAnnouncingForHostname("MyTestHost")
+ val inOrder = inOrder(prober, announcer)
+
+ // Remove the custom host: the custom host's announcement is stopped and the probed services
+ // which use that hostname are re-announced.
+ advertiser.removeService(TEST_SERVICE_ID_1)
+
+ inOrder.verify(prober).stop(TEST_SERVICE_ID_1)
+ inOrder.verify(announcer).stop(TEST_SERVICE_ID_1)
+ inOrder.verify(announcer).stop(TEST_SERVICE_ID_2)
+ inOrder.verify(announcer).startSending(TEST_SERVICE_ID_2, announcementInfo, 0L /* initialDelayMs */)
+ }
+
+ @Test
+ fun testAddMoreAddressesForCustomHost_restartAnnouncingForProbedServices() {
+ val customHost = NsdServiceInfo().apply {
+ hostname = "MyTestHost"
+ hostAddresses = listOf(
+ parseNumericAddress("192.0.2.23"),
+ parseNumericAddress("2001:db8::1"))
+ }
+ doReturn("MyTestHost")
+ .`when`(repository).getHostnameForServiceId(TEST_SERVICE_ID_1)
+ doReturn("MyTestHost")
+ .`when`(repository).getHostnameForServiceId(TEST_SERVICE_ID_2)
+ val announcementInfo1 =
+ addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1_CUSTOM_HOST)
+
+ val probingInfo2 = addServiceAndStartProbing(TEST_SERVICE_ID_2, customHost)
+ val announcementInfo2 = AnnouncementInfo(TEST_SERVICE_ID_2, emptyList(), emptyList())
+ doReturn(announcementInfo2).`when`(repository).onProbingSucceeded(probingInfo2)
+ doReturn(listOf(announcementInfo1, announcementInfo2))
+ .`when`(repository).restartAnnouncingForHostname("MyTestHost")
+ probeCb.onFinished(probingInfo2)
+
+ val inOrder = inOrder(prober, announcer)
+
+ inOrder.verify(announcer)
+ .startSending(TEST_SERVICE_ID_2, announcementInfo2, 0L /* initialDelayMs */)
+ inOrder.verify(announcer).stop(TEST_SERVICE_ID_1)
+ inOrder.verify(announcer)
+ .startSending(TEST_SERVICE_ID_1, announcementInfo1, 0L /* initialDelayMs */)
}
@Test
@@ -199,7 +300,8 @@
fun testReplyToQuery() {
addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- val testReply = MdnsReplyInfo(emptyList(), emptyList(), 0, InetSocketAddress(0))
+ val testReply = MdnsReplyInfo(emptyList(), emptyList(), 0, InetSocketAddress(0),
+ InetSocketAddress(0), emptyList())
doReturn(testReply).`when`(repository).getReply(any(), any())
// Query obtained with:
@@ -213,7 +315,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)
@@ -229,9 +336,116 @@
}
@Test
+ fun testReplyToQuery_TruncatedBitSet() {
+ addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ val src = InetSocketAddress(parseNumericAddress("2001:db8::456"), MdnsConstants.MDNS_PORT)
+ val testReply = MdnsReplyInfo(emptyList(), emptyList(), 400L, InetSocketAddress(0), src,
+ emptyList())
+ val knownAnswersReply = MdnsReplyInfo(emptyList(), emptyList(), 400L, InetSocketAddress(0),
+ src, emptyList())
+ val knownAnswersReply2 = MdnsReplyInfo(emptyList(), emptyList(), 0L, InetSocketAddress(0),
+ src, emptyList())
+ doReturn(testReply).`when`(repository).getReply(
+ argThat { pkg -> pkg.questions.size != 0 && pkg.answers.size == 0 &&
+ (pkg.flags and MdnsConstants.FLAG_TRUNCATED) != 0},
+ eq(src))
+ doReturn(knownAnswersReply).`when`(repository).getReply(
+ argThat { pkg -> pkg.questions.size == 0 && pkg.answers.size != 0 &&
+ (pkg.flags and MdnsConstants.FLAG_TRUNCATED) != 0},
+ eq(src))
+ doReturn(knownAnswersReply2).`when`(repository).getReply(
+ argThat { pkg -> pkg.questions.size == 0 && pkg.answers.size != 0 &&
+ (pkg.flags and MdnsConstants.FLAG_TRUNCATED) == 0},
+ eq(src))
+
+ // Query obtained with:
+ // scapy.raw(scapy.DNS(
+ // tc = 1, qd = scapy.DNSQR(qtype='PTR', qname='_testservice._tcp.local'))
+ // ).hex().upper()
+ val query = HexDump.hexStringToByteArray(
+ "0000030000010000000000000C5F7465737473657276696365045F746370056C6F63616C00000C0001"
+ )
+
+ packetHandler.handlePacket(query, query.size, src)
+
+ val packetCaptor = ArgumentCaptor.forClass(MdnsPacket::class.java)
+ verify(repository).getReply(packetCaptor.capture(), eq(src))
+
+ packetCaptor.value.let {
+ assertTrue((it.flags and MdnsConstants.FLAG_TRUNCATED) != 0)
+ assertEquals(1, it.questions.size)
+ assertEquals(0, it.answers.size)
+ assertEquals(0, it.authorityRecords.size)
+ assertEquals(0, it.additionalRecords.size)
+
+ assertTrue(it.questions[0] is MdnsPointerRecord)
+ assertContentEquals(arrayOf("_testservice", "_tcp", "local"), it.questions[0].name)
+ }
+
+ verify(replySender).queueReply(testReply)
+
+ // Known-Answer packet with truncated bit set obtained with:
+ // scapy.raw(scapy.DNS(
+ // tc = 1, qd = None, an = scapy.DNSRR(type='PTR', rrname='_testtype._tcp.local',
+ // rdata='othertestservice._testtype._tcp.local', rclass='IN', ttl=4500))
+ // ).hex().upper()
+ val knownAnswers = HexDump.hexStringToByteArray(
+ "000003000000000100000000095F7465737474797065045F746370056C6F63616C00000C0001000" +
+ "011940027106F746865727465737473657276696365095F7465737474797065045F7463" +
+ "70056C6F63616C00"
+ )
+
+ packetHandler.handlePacket(knownAnswers, knownAnswers.size, src)
+
+ verify(repository, times(2)).getReply(packetCaptor.capture(), eq(src))
+
+ packetCaptor.value.let {
+ assertTrue((it.flags and MdnsConstants.FLAG_TRUNCATED) != 0)
+ assertEquals(0, it.questions.size)
+ assertEquals(1, it.answers.size)
+ assertEquals(0, it.authorityRecords.size)
+ assertEquals(0, it.additionalRecords.size)
+
+ assertTrue(it.answers[0] is MdnsPointerRecord)
+ assertContentEquals(arrayOf("_testtype", "_tcp", "local"), it.answers[0].name)
+ }
+
+ verify(replySender).queueReply(knownAnswersReply)
+
+ // Known-Answer packet obtained with:
+ // scapy.raw(scapy.DNS(
+ // qd = None, an = scapy.DNSRR(type='PTR', rrname='_testtype._tcp.local',
+ // rdata='testservice._testtype._tcp.local', rclass='IN', ttl=4500))
+ // ).hex().upper()
+ val knownAnswers2 = HexDump.hexStringToByteArray(
+ "000001000000000100000000095F7465737474797065045F746370056C6F63616C00000C0001000" +
+ "0119400220B7465737473657276696365095F7465737474797065045F746370056C6F63" +
+ "616C00"
+ )
+
+ packetHandler.handlePacket(knownAnswers2, knownAnswers2.size, src)
+
+ verify(repository, times(3)).getReply(packetCaptor.capture(), eq(src))
+
+ packetCaptor.value.let {
+ assertTrue((it.flags and MdnsConstants.FLAG_TRUNCATED) == 0)
+ assertEquals(0, it.questions.size)
+ assertEquals(1, it.answers.size)
+ assertEquals(0, it.authorityRecords.size)
+ assertEquals(0, it.additionalRecords.size)
+
+ assertTrue(it.answers[0] is MdnsPointerRecord)
+ assertContentEquals(arrayOf("_testtype", "_tcp", "local"), it.answers[0].name)
+ }
+
+ verify(replySender).queueReply(knownAnswersReply2)
+ }
+
+ @Test
fun testConflict() {
addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- doReturn(setOf(TEST_SERVICE_ID_1)).`when`(repository).getConflictingServices(any())
+ doReturn(mapOf(TEST_SERVICE_ID_1 to CONFLICT_SERVICE))
+ .`when`(repository).getConflictingServices(any())
// Reply obtained with:
// scapy.raw(scapy.DNS(
@@ -257,7 +471,7 @@
}
thread.waitForIdle(TIMEOUT_MS)
- verify(cb).onServiceConflict(advertiser, TEST_SERVICE_ID_1)
+ verify(cb).onServiceConflict(advertiser, TEST_SERVICE_ID_1, CONFLICT_SERVICE)
}
@Test
@@ -284,9 +498,10 @@
@Test
fun testReplaceExitingService() {
doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
- .addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
- advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1_SUBTYPE)
- verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+ .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
+ advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1_SUBTYPE,
+ MdnsAdvertisingOptions.getDefaultOptions())
+ verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
verify(announcer).stop(TEST_SERVICE_ID_DUPLICATE)
verify(prober).startProbing(any())
}
@@ -294,7 +509,7 @@
@Test
fun testUpdateExistingService() {
doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
- .addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+ .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
val subTypes = setOf("_sub")
advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subTypes)
verify(repository).updateService(eq(TEST_SERVICE_ID_DUPLICATE), any())
@@ -302,18 +517,25 @@
verify(prober, never()).startProbing(any())
}
- private fun addServiceAndFinishProbing(serviceId: Int, serviceInfo: NsdServiceInfo):
- AnnouncementInfo {
+ private fun addServiceAndStartProbing(serviceId: Int, serviceInfo: NsdServiceInfo):
+ ProbingInfo {
val testProbingInfo = mock(ProbingInfo::class.java)
doReturn(serviceId).`when`(testProbingInfo).serviceId
doReturn(testProbingInfo).`when`(repository).setServiceProbing(serviceId)
- advertiser.addService(serviceId, serviceInfo)
- verify(repository).addService(serviceId, serviceInfo)
+ advertiser.addService(serviceId, serviceInfo, MdnsAdvertisingOptions.getDefaultOptions())
+ verify(repository).addService(serviceId, serviceInfo, null /* ttl */)
verify(prober).startProbing(testProbingInfo)
+ return testProbingInfo
+ }
+
+ private fun addServiceAndFinishProbing(serviceId: Int, serviceInfo: NsdServiceInfo):
+ AnnouncementInfo {
+ val testProbingInfo = addServiceAndStartProbing(serviceId, serviceInfo)
+
// Simulate probing success: continues to announcing
- val testAnnouncementInfo = mock(AnnouncementInfo::class.java)
+ val testAnnouncementInfo = AnnouncementInfo(serviceId, emptyList(), emptyList())
doReturn(testAnnouncementInfo).`when`(repository).onProbingSucceeded(testProbingInfo)
probeCb.onFinished(testProbingInfo)
return testAnnouncementInfo
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..fb3d183 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -23,6 +23,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
@@ -47,6 +48,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -55,6 +57,7 @@
import java.net.DatagramPacket;
import java.net.NetworkInterface;
import java.net.SocketException;
+import java.util.ArrayList;
import java.util.List;
@RunWith(DevSdkIgnoreRunner.class)
@@ -101,11 +104,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();
}
@@ -148,7 +157,7 @@
verify(mSocketCreationCallback).onSocketCreated(tetherSocketKey2);
// Send packet to IPv4 with mSocketKey and verify sending has been called.
- mSocketClient.sendPacketRequestingMulticastResponse(ipv4Packet, mSocketKey,
+ mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), mSocketKey,
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
verify(mSocket).send(ipv4Packet);
@@ -156,7 +165,7 @@
verify(tetherIfaceSock2, never()).send(any());
// Send packet to IPv4 with onlyUseIpv6OnIpv6OnlyNetworks = true, the packet will be sent.
- mSocketClient.sendPacketRequestingMulticastResponse(ipv4Packet, mSocketKey,
+ mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), mSocketKey,
true /* onlyUseIpv6OnIpv6OnlyNetworks */);
HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
verify(mSocket, times(2)).send(ipv4Packet);
@@ -164,7 +173,7 @@
verify(tetherIfaceSock2, never()).send(any());
// Send packet to IPv6 with tetherSocketKey1 and verify sending has been called.
- mSocketClient.sendPacketRequestingMulticastResponse(ipv6Packet, tetherSocketKey1,
+ mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv6Packet), tetherSocketKey1,
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
verify(mSocket, never()).send(ipv6Packet);
@@ -174,7 +183,7 @@
// Send packet to IPv6 with onlyUseIpv6OnIpv6OnlyNetworks = true, the packet will not be
// sent. Therefore, the tetherIfaceSock1.send() and tetherIfaceSock2.send() are still be
// called once.
- mSocketClient.sendPacketRequestingMulticastResponse(ipv6Packet, tetherSocketKey1,
+ mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv6Packet), tetherSocketKey1,
true /* onlyUseIpv6OnIpv6OnlyNetworks */);
HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
verify(mSocket, never()).send(ipv6Packet);
@@ -260,7 +269,7 @@
verify(mSocketCreationCallback).onSocketCreated(socketKey3);
// Send IPv4 packet on the mSocketKey and verify sending has been called.
- mSocketClient.sendPacketRequestingMulticastResponse(ipv4Packet, mSocketKey,
+ mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), mSocketKey,
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
verify(mSocket).send(ipv4Packet);
@@ -289,7 +298,7 @@
verify(socketCreationCb2).onSocketCreated(socketKey3);
// Send IPv4 packet on socket2 and verify sending to the socket2 only.
- mSocketClient.sendPacketRequestingMulticastResponse(ipv4Packet, socketKey2,
+ mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), socketKey2,
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
// ipv4Packet still sent only once on mSocket: times(1) matches the packet sent earlier on
@@ -303,7 +312,7 @@
verify(mProvider, timeout(DEFAULT_TIMEOUT)).unrequestSocket(callback2);
// Send IPv4 packet again and verify it's still sent a second time
- mSocketClient.sendPacketRequestingMulticastResponse(ipv4Packet, socketKey2,
+ mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), socketKey2,
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
verify(socket2, times(2)).send(ipv4Packet);
@@ -314,7 +323,7 @@
verify(mProvider, timeout(DEFAULT_TIMEOUT)).unrequestSocket(callback);
// Send IPv4 packet and verify no more sending.
- mSocketClient.sendPacketRequestingMulticastResponse(ipv4Packet, mSocketKey,
+ mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet), mSocketKey,
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
verify(mSocket, times(1)).send(ipv4Packet);
@@ -365,4 +374,67 @@
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);
+ }
+
+ @Test
+ public void testSendPacketWithMultipleDatagramPacket() throws IOException {
+ final SocketCallback callback = expectSocketCallback();
+ final List<DatagramPacket> packets = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ packets.add(new DatagramPacket(new byte[10 + i] /* buff */, 0 /* offset */,
+ 10 + i /* length */, MdnsConstants.IPV4_SOCKET_ADDR));
+ }
+ doReturn(true).when(mSocket).hasJoinedIpv4();
+ doReturn(true).when(mSocket).hasJoinedIpv6();
+ doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+
+ // Notify socket created
+ callback.onSocketCreated(mSocketKey, mSocket, List.of());
+ verify(mSocketCreationCallback).onSocketCreated(mSocketKey);
+
+ // Send packets to IPv4 with mSocketKey then verify sending has been called and the
+ // sequence is correct.
+ mSocketClient.sendPacketRequestingMulticastResponse(packets, mSocketKey,
+ false /* onlyUseIpv6OnIpv6OnlyNetworks */);
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ InOrder inOrder = inOrder(mSocket);
+ for (int i = 0; i < 10; i++) {
+ inOrder.verify(mSocket).send(packets.get(i));
+ }
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
index 5b7c0ba..9befbc1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
@@ -61,6 +61,7 @@
private val cb = mock(MdnsPacketRepeater.PacketRepeaterCallback::class.java)
as MdnsPacketRepeater.PacketRepeaterCallback<ProbingInfo>
private val buffer = ByteArray(1500)
+ private val flags = MdnsFeatureFlags.newBuilder().build()
@Before
fun setUp() {
@@ -120,7 +121,7 @@
@Test
fun testProbe() {
val replySender = MdnsReplySender(
- thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */, flags)
val prober = TestProber(thread.looper, replySender, cb, sharedLog)
val probeInfo = TestProbeInfo(
listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890)))
@@ -145,7 +146,7 @@
@Test
fun testProbeMultipleRecords() {
val replySender = MdnsReplySender(
- thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */, flags)
val prober = TestProber(thread.looper, replySender, cb, sharedLog)
val probeInfo = TestProbeInfo(listOf(
makeServiceRecord(TEST_SERVICE_NAME_1, 37890),
@@ -184,7 +185,7 @@
@Test
fun testStopProbing() {
val replySender = MdnsReplySender(
- thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */, flags)
val prober = TestProber(thread.looper, replySender, cb, sharedLog)
val probeInfo = TestProbeInfo(
listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890)),
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 4b1f166..271cc65 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -22,9 +22,11 @@
import android.os.Build
import android.os.HandlerThread
import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo
+import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_HOST
+import com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE
+import com.android.server.connectivity.mdns.MdnsProber.ProbingInfo
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_ANY
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
@@ -37,6 +39,7 @@
import java.net.InetSocketAddress
import java.net.NetworkInterface
import java.util.Collections
+import java.time.Duration
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@@ -49,10 +52,17 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
private const val TEST_SERVICE_ID_1 = 42
private const val TEST_SERVICE_ID_2 = 43
private const val TEST_SERVICE_ID_3 = 44
+private const val TEST_CUSTOM_HOST_ID_1 = 45
+private const val TEST_CUSTOM_HOST_ID_2 = 46
+private const val TEST_SERVICE_CUSTOM_HOST_ID_1 = 48
private const val TEST_PORT = 12345
private const val TEST_SUBTYPE = "_subtype"
private const val TEST_SUBTYPE2 = "_subtype2"
@@ -87,6 +97,34 @@
port = TEST_PORT
}
+private val TEST_CUSTOM_HOST_1 = NsdServiceInfo().apply {
+ hostname = "TestHost"
+ hostAddresses = listOf(parseNumericAddress("2001:db8::1"), parseNumericAddress("2001:db8::2"))
+}
+
+private val TEST_CUSTOM_HOST_1_NAME = arrayOf("TestHost", "local")
+
+private val TEST_CUSTOM_HOST_2 = NsdServiceInfo().apply {
+ hostname = "OtherTestHost"
+ hostAddresses = listOf(parseNumericAddress("2001:db8::3"), parseNumericAddress("2001:db8::4"))
+}
+
+private val TEST_SERVICE_CUSTOM_HOST_1 = NsdServiceInfo().apply {
+ hostname = "TestHost"
+ hostAddresses = listOf(parseNumericAddress("2001:db8::1"))
+ serviceType = "_testservice._tcp"
+ serviceName = "TestService"
+ port = TEST_PORT
+}
+
+private val TEST_SERVICE_CUSTOM_HOST_NO_ADDRESSES = NsdServiceInfo().apply {
+ hostname = "TestHost"
+ hostAddresses = listOf()
+ serviceType = "_testservice._tcp"
+ serviceName = "TestService"
+ port = TEST_PORT
+}
+
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsRecordRepositoryTest {
@@ -95,7 +133,6 @@
override fun getInterfaceInetAddresses(iface: NetworkInterface) =
Collections.enumeration(TEST_ADDRESSES.map { it.address })
}
- private val flags = MdnsFeatureFlags.newBuilder().build()
@Before
fun setUp() {
@@ -108,11 +145,22 @@
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.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, Duration.ofSeconds(50)))
assertEquals(1, repository.servicesCount)
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -135,7 +183,7 @@
assertEquals(MdnsServiceRecord(expectedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- SHORT_TTL /* ttlMillis */,
+ 50_000L /* ttlMillis */,
0 /* servicePriority */, 0 /* serviceWeight */,
TEST_PORT, TEST_HOSTNAME), packet.authorityRecords[0])
@@ -144,19 +192,19 @@
@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)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* ttl */)
}
assertFailsWith(NameConflictException::class) {
- repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3)
+ repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3, null /* ttl */)
}
}
@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) {
@@ -167,8 +215,8 @@
val queriedName = 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 */)
+ val query = MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+ emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val reply = repository.getReply(query, src)
@@ -190,19 +238,19 @@
@Test
fun testInvalidReuseOfServiceId() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
assertFailsWith(IllegalArgumentException::class) {
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* ttl */)
}
}
@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)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1))
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -216,7 +264,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 */)
@@ -246,7 +294,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 */)
@@ -288,13 +336,13 @@
@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)
assertEquals(TEST_SERVICE_ID_1,
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1))
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* ttl */))
assertEquals(1, repository.servicesCount)
repository.removeService(TEST_SERVICE_ID_2)
@@ -303,7 +351,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 */)
@@ -435,7 +483,7 @@
@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")
@@ -497,13 +545,13 @@
@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 */))
val queryCaseInsensitive = MdnsPacket(0 /* flags */, questionsCaseInSensitive,
- listOf() /* answers */, listOf() /* authorityRecords */,
- listOf() /* additionalRecords */)
+ emptyList() /* answers */, emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val replyCaseInsensitive = repository.getReply(queryCaseInsensitive, src)
assertNotNull(replyCaseInsensitive)
@@ -516,8 +564,8 @@
*/
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 */)
+ return MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+ emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
}
private fun makeQuestionRecord(name: Array<String>, type: Int): MdnsRecord {
@@ -532,7 +580,7 @@
@Test
fun testGetReply_singlePtrQuestion_returnsSrvTxtAddressNsecRecords() {
- 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))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
@@ -546,7 +594,7 @@
arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName)),
reply.answers)
assertEquals(listOf(
- MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
MdnsInetAddressRecord(
TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
@@ -561,9 +609,95 @@
), reply.additionalAnswers)
}
+
+ @Test
+ fun testGetReply_ptrQuestionForServiceWithCustomHost_customHostUsedInAdditionalAnswers() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_SERVICE_CUSTOM_HOST_ID_1, TEST_SERVICE_CUSTOM_HOST_1,
+ setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
+ val src = InetSocketAddress(parseNumericAddress("fe80::1234"), 5353)
+ val serviceName = arrayOf("TestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(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_CUSTOM_HOST_1_NAME),
+ MdnsInetAddressRecord(
+ TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+ parseNumericAddress("2001:db8::1")),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+ TEST_CUSTOM_HOST_1_NAME /* nextDomain */,
+ intArrayOf(TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_ptrQuestionForServicesWithSameCustomHost_customHostUsedInAdditionalAnswers() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ val serviceWithCustomHost1 = NsdServiceInfo().apply {
+ hostname = "TestHost"
+ hostAddresses = listOf(
+ parseNumericAddress("2001:db8::1"),
+ parseNumericAddress("192.0.2.1"))
+ serviceType = "_testservice._tcp"
+ serviceName = "TestService1"
+ port = TEST_PORT
+ }
+ val serviceWithCustomHost2 = NsdServiceInfo().apply {
+ hostname = "TestHost"
+ hostAddresses = listOf(
+ parseNumericAddress("2001:db8::1"),
+ parseNumericAddress("2001:db8::3"))
+ }
+ repository.addServiceAndFinishProbing(TEST_SERVICE_ID_1, serviceWithCustomHost1)
+ repository.addServiceAndFinishProbing(TEST_SERVICE_ID_2, serviceWithCustomHost2)
+ val src = InetSocketAddress(parseNumericAddress("fe80::1234"), 5353)
+ val serviceName = arrayOf("TestService1", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(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_CUSTOM_HOST_1_NAME),
+ MdnsInetAddressRecord(
+ TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+ parseNumericAddress("2001:db8::1")),
+ MdnsInetAddressRecord(
+ TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+ parseNumericAddress("192.0.2.1")),
+ MdnsInetAddressRecord(
+ TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+ parseNumericAddress("2001:db8::3")),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_CUSTOM_HOST_1_NAME, 0L, true, SHORT_TTL,
+ TEST_CUSTOM_HOST_1_NAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
@Test
fun testGetReply_singleSubtypePtrQuestion_returnsSrvTxtAddressNsecRecords() {
- 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))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
@@ -579,7 +713,7 @@
LONG_TTL, serviceName)),
reply.answers)
assertEquals(listOf(
- MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
MdnsInetAddressRecord(
TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
@@ -596,7 +730,7 @@
@Test
fun testGetReply_duplicatePtrQuestions_doesNotReturnDuplicateRecords() {
- 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))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
@@ -612,7 +746,7 @@
arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName)),
reply.answers)
assertEquals(listOf(
- MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
MdnsInetAddressRecord(
TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
@@ -629,7 +763,7 @@
@Test
fun testGetReply_multiplePtrQuestionsWithSubtype_doesNotReturnDuplicateRecords() {
- 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))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
@@ -648,7 +782,7 @@
0L, false, LONG_TTL, serviceName)),
reply.answers)
assertEquals(listOf(
- MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
MdnsInetAddressRecord(
TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
@@ -665,7 +799,7 @@
@Test
fun testGetReply_txtQuestion_returnsNoNsecRecord() {
- 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))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
@@ -674,7 +808,7 @@
val reply = repository.getReply(query, src)
assertNotNull(reply)
- assertEquals(listOf(MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf())),
+ assertEquals(listOf(MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList())),
reply.answers)
// No NSEC records because the reply doesn't include the SRV record
assertTrue(reply.additionalAnswers.isEmpty())
@@ -682,7 +816,7 @@
@Test
fun testGetReply_AAAAQuestionButNoIpv6Address_returnsNsecRecord() {
- 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),
listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
@@ -700,8 +834,93 @@
}
@Test
+ fun testGetReply_AAAAQuestionForCustomHost_returnsAAAARecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(
+ TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, subtypes = setOf(),
+ listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+ val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353)
+
+ val query = makeQuery(TYPE_AAAA to TEST_CUSTOM_HOST_1_NAME)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsInetAddressRecord(TEST_CUSTOM_HOST_1_NAME,
+ 0, false, LONG_TTL, parseNumericAddress("2001:db8::1")),
+ MdnsInetAddressRecord(TEST_CUSTOM_HOST_1_NAME,
+ 0, false, LONG_TTL, parseNumericAddress("2001:db8::2"))),
+ reply.answers)
+ assertEquals(
+ listOf(MdnsNsecRecord(TEST_CUSTOM_HOST_1_NAME,
+ 0L, true, SHORT_TTL,
+ TEST_CUSTOM_HOST_1_NAME /* nextDomain */,
+ intArrayOf(TYPE_AAAA))),
+ reply.additionalAnswers)
+ }
+
+
+ @Test
+ fun testGetReply_AAAAQuestionForCustomHostInMultipleRegistrations_returnsAAAARecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+
+ repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_1, NsdServiceInfo().apply {
+ hostname = "TestHost"
+ hostAddresses = listOf(
+ parseNumericAddress("2001:db8::1"),
+ parseNumericAddress("2001:db8::2"))
+ })
+ repository.addServiceAndFinishProbing(TEST_CUSTOM_HOST_ID_2, NsdServiceInfo().apply {
+ hostname = "TestHost"
+ hostAddresses = listOf(
+ parseNumericAddress("2001:db8::1"),
+ parseNumericAddress("2001:db8::3"))
+ })
+ val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353)
+
+ val query = makeQuery(TYPE_AAAA to TEST_CUSTOM_HOST_1_NAME)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsInetAddressRecord(TEST_CUSTOM_HOST_1_NAME,
+ 0, false, LONG_TTL, parseNumericAddress("2001:db8::1")),
+ MdnsInetAddressRecord(TEST_CUSTOM_HOST_1_NAME,
+ 0, false, LONG_TTL, parseNumericAddress("2001:db8::2")),
+ MdnsInetAddressRecord(TEST_CUSTOM_HOST_1_NAME,
+ 0, false, LONG_TTL, parseNumericAddress("2001:db8::3"))),
+ reply.answers)
+ assertEquals(
+ listOf(MdnsNsecRecord(TEST_CUSTOM_HOST_1_NAME,
+ 0L, true, SHORT_TTL,
+ TEST_CUSTOM_HOST_1_NAME /* nextDomain */,
+ intArrayOf(TYPE_AAAA))),
+ reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_customHostRemoved_noAnswerToAAAAQuestion() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(
+ TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, subtypes = setOf(),
+ listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
+ repository.addService(
+ TEST_SERVICE_CUSTOM_HOST_ID_1, TEST_SERVICE_CUSTOM_HOST_1, null /* ttl */)
+ repository.removeService(TEST_CUSTOM_HOST_ID_1)
+ repository.removeService(TEST_SERVICE_CUSTOM_HOST_ID_1)
+
+ val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353)
+
+ val query = makeQuery(TYPE_AAAA to TEST_CUSTOM_HOST_1_NAME)
+ val reply = repository.getReply(query, src)
+
+ assertNull(reply)
+ }
+
+ @Test
fun testGetReply_ptrAndSrvQuestions_doesNotReturnSrvRecordInAdditionalAnswerSection() {
- 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))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
@@ -723,7 +942,7 @@
@Test
fun testGetReply_srvTxtAddressQuestions_returnsAllRecordsInAnswerSectionExceptNsec() {
- 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))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
@@ -739,7 +958,7 @@
assertNotNull(reply)
assertEquals(listOf(
MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
- MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, emptyList()),
MdnsInetAddressRecord(
TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
MdnsInetAddressRecord(
@@ -757,7 +976,7 @@
@Test
fun testGetReply_queryWithIpv4Address_replyWithIpv4Address() {
- 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))
val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
@@ -771,7 +990,7 @@
@Test
fun testGetReply_queryWithIpv6Address_replyWithIpv6Address() {
- 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))
val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
@@ -785,9 +1004,9 @@
@Test
fun testGetConflictingServices() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
val packet = MdnsPacket(
0 /* flags */,
@@ -807,15 +1026,18 @@
emptyList() /* authorityRecords */,
emptyList() /* additionalRecords */)
- assertEquals(setOf(TEST_SERVICE_ID_1, TEST_SERVICE_ID_2),
+ assertEquals(
+ mapOf(
+ TEST_SERVICE_ID_1 to CONFLICT_SERVICE,
+ TEST_SERVICE_ID_2 to CONFLICT_SERVICE),
repository.getConflictingServices(packet))
}
@Test
fun testGetConflictingServicesCaseInsensitive() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
val packet = MdnsPacket(
0 /* flags */,
@@ -835,15 +1057,138 @@
emptyList() /* authorityRecords */,
emptyList() /* additionalRecords */)
- assertEquals(setOf(TEST_SERVICE_ID_1, TEST_SERVICE_ID_2),
- repository.getConflictingServices(packet))
+ assertEquals(
+ mapOf(TEST_SERVICE_ID_1 to CONFLICT_SERVICE,
+ TEST_SERVICE_ID_2 to CONFLICT_SERVICE),
+ repository.getConflictingServices(packet))
+ }
+
+ @Test
+ fun testGetConflictingServices_customHosts_differentAddresses() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+
+ val packet = MdnsPacket(
+ 0, /* flags */
+ emptyList(), /* questions */
+ listOf(
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::5")),
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::6")),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+
+ assertEquals(mapOf(TEST_CUSTOM_HOST_ID_1 to CONFLICT_HOST),
+ repository.getConflictingServices(packet))
+ }
+
+ @Test
+ fun testGetConflictingServices_customHosts_moreAddressesThanUs_conflict() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+
+ val packet = MdnsPacket(
+ 0, /* flags */
+ emptyList(), /* questions */
+ listOf(
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::1")),
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::3")),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+
+ assertEquals(mapOf(TEST_CUSTOM_HOST_ID_1 to CONFLICT_HOST),
+ repository.getConflictingServices(packet))
+ }
+
+ @Test
+ fun testGetConflictingServices_customHostsReplyHasFewerAddressesThanUs_noConflict() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+
+ val packet = MdnsPacket(
+ 0, /* flags */
+ emptyList(), /* questions */
+ listOf(
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+
+ assertEquals(emptyMap(),
+ repository.getConflictingServices(packet))
+ }
+
+ @Test
+ fun testGetConflictingServices_customHostsReplyHasIdenticalHosts_noConflict() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+
+ val packet = MdnsPacket(
+ 0, /* flags */
+ emptyList(), /* questions */
+ listOf(
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::1")),
+ MdnsInetAddressRecord(arrayOf("TestHost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+
+ assertEquals(emptyMap(),
+ repository.getConflictingServices(packet))
+ }
+
+
+ @Test
+ fun testGetConflictingServices_customHostsCaseInsensitiveReplyHasIdenticalHosts_noConflict() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+ repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
+
+ val packet = MdnsPacket(
+ 0, /* flags */
+ emptyList(), /* questions */
+ listOf(
+ MdnsInetAddressRecord(arrayOf("TESTHOST", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::1")),
+ MdnsInetAddressRecord(arrayOf("testhost", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ 0L /* ttlMillis */, parseNumericAddress("2001:db8::2")),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+
+ assertEquals(emptyMap(),
+ repository.getConflictingServices(packet))
}
@Test
fun testGetConflictingServices_IdenticalService() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
val otherTtlMillis = 1234L
val packet = MdnsPacket(
@@ -865,14 +1210,14 @@
emptyList() /* additionalRecords */)
// Above records are identical to the actual registrations: no conflict
- assertEquals(emptySet(), repository.getConflictingServices(packet))
+ assertEquals(emptyMap(), repository.getConflictingServices(packet))
}
@Test
fun testGetConflictingServicesCaseInsensitive_IdenticalService() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
val otherTtlMillis = 1234L
val packet = MdnsPacket(
@@ -894,12 +1239,12 @@
emptyList() /* additionalRecords */)
// Above records are identical to the actual registrations: no conflict
- assertEquals(emptySet(), repository.getConflictingServices(packet))
+ assertEquals(emptyMap(), repository.getConflictingServices(packet))
}
@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,
@@ -907,8 +1252,8 @@
val questions = listOf(
MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"), false /* isUnicast */))
- val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
- listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ val query = MdnsPacket(0 /* flags */, questions, emptyList() /* answers */,
+ emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
// Reply to the question and verify there is one packet replied.
@@ -924,10 +1269,11 @@
@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))
+ assertEquals(-1,
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */))
assertEquals(1, repository.servicesCount)
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -986,18 +1332,17 @@
questions: List<MdnsRecord>,
knownAnswers: List<MdnsRecord>,
replyAnswers: List<MdnsRecord>,
- additionalAnswers: List<MdnsRecord>,
- expectReply: Boolean
+ additionalAnswers: List<MdnsRecord>
) {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
- MdnsFeatureFlags.newBuilder().setIsKnownAnswerSuppressionEnabled(true).build())
+ makeFlags(isKnownAnswerSuppressionEnabled = true))
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
val query = MdnsPacket(0 /* flags */, questions, knownAnswers,
- listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ emptyList() /* authorityRecords */, emptyList() /* additionalRecords */)
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
val reply = repository.getReply(query, src)
- if (!expectReply) {
+ if (replyAnswers.isEmpty() || additionalAnswers.isEmpty()) {
assertNull(reply)
return
}
@@ -1008,6 +1353,7 @@
assertEquals(MdnsConstants.MDNS_PORT, reply.destination.port)
assertEquals(replyAnswers, reply.answers)
assertEquals(additionalAnswers, reply.additionalAnswers)
+ assertEquals(knownAnswers, reply.knownAnswers)
}
@Test
@@ -1020,8 +1366,8 @@
false /* cacheFlush */,
LONG_TTL,
arrayOf("MyTestService", "_testservice", "_tcp", "local")))
- doGetReplyWithAnswersTest(questions, knownAnswers, listOf() /* replyAnswers */,
- listOf() /* additionalAnswers */, false /* expectReply */)
+ doGetReplyWithAnswersTest(questions, knownAnswers, emptyList() /* replyAnswers */,
+ emptyList() /* additionalAnswers */)
}
@Test
@@ -1047,7 +1393,7 @@
0L /* receiptTimeMillis */,
true /* cacheFlush */,
LONG_TTL,
- listOf() /* entries */),
+ emptyList() /* entries */),
MdnsServiceRecord(
serviceName,
0L /* receiptTimeMillis */,
@@ -1089,8 +1435,7 @@
SHORT_TTL,
TEST_HOSTNAME /* nextDomain */,
intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
- doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers,
- true /* expectReply */)
+ doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers)
}
@Test
@@ -1116,7 +1461,7 @@
0L /* receiptTimeMillis */,
true /* cacheFlush */,
LONG_TTL,
- listOf() /* entries */),
+ emptyList() /* entries */),
MdnsServiceRecord(
serviceName,
0L /* receiptTimeMillis */,
@@ -1158,8 +1503,7 @@
SHORT_TTL,
TEST_HOSTNAME /* nextDomain */,
intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
- doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers,
- true /* expectReply */)
+ doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers)
}
@Test
@@ -1210,8 +1554,7 @@
SHORT_TTL,
TEST_HOSTNAME /* nextDomain */,
intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
- doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers,
- true /* expectReply */)
+ doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers)
}
@Test
@@ -1222,23 +1565,250 @@
MdnsPointerRecord(queriedName, false /* isUnicast */),
MdnsServiceRecord(serviceName, false /* isUnicast */))
val knownAnswers = listOf(
- MdnsPointerRecord(
- queriedName,
- 0L /* receiptTimeMillis */,
- false /* cacheFlush */,
- LONG_TTL - 1000L,
- serviceName),
+ 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, emptyList() /* replyAnswers */,
+ emptyList() /* additionalAnswers */)
+ }
+
+ @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, emptyList() /* answers */,
+ emptyList() /* authorityRecords */, emptyList() /* 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, emptyList() /* answers */,
+ emptyList() /* authorityRecords */, emptyList() /* 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, emptyList() /* answers */,
+ emptyList() /* authorityRecords */, emptyList() /* 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, emptyList() /* answers */,
+ emptyList() /* authorityRecords */, emptyList() /* 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 testGetReply_OnlyKnownAnswers() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
+ makeFlags(isKnownAnswerSuppressionEnabled = true))
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ val knownAnswers = listOf(MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL - 1000L,
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")))
+ val query = MdnsPacket(MdnsConstants.FLAG_TRUNCATED /* flags */, emptyList(),
+ knownAnswers, emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val reply = repository.getReply(query, src)
+ assertNotNull(reply)
+ assertEquals(0, reply.answers.size)
+ assertEquals(0, reply.additionalAnswers.size)
+ assertEquals(knownAnswers, reply.knownAnswers)
+ }
+
+ @Test
+ fun testRestartProbingForHostname() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1,
+ setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
+ repository.addService(TEST_SERVICE_CUSTOM_HOST_ID_1,
+ TEST_SERVICE_CUSTOM_HOST_NO_ADDRESSES, null)
+ repository.setServiceProbing(TEST_SERVICE_CUSTOM_HOST_ID_1)
+ repository.removeService(TEST_CUSTOM_HOST_ID_1)
+
+ val probingInfos = repository.restartProbingForHostname("TestHost")
+
+ assertEquals(1, probingInfos.size)
+ val probingInfo = probingInfos.get(0)
+ assertEquals(TEST_SERVICE_CUSTOM_HOST_ID_1, probingInfo.serviceId)
+ val packet = probingInfo.getPacket(0)
+ assertEquals(0, packet.transactionId)
+ assertEquals(MdnsConstants.FLAGS_QUERY, packet.flags)
+ assertEquals(0, packet.answers.size)
+ assertEquals(0, packet.additionalRecords.size)
+ assertEquals(1, packet.questions.size)
+ val serviceName = arrayOf("TestService", "_testservice", "_tcp", "local")
+ assertEquals(MdnsAnyRecord(serviceName, false /* unicast */), packet.questions[0])
+ assertThat(packet.authorityRecords).containsExactly(
MdnsServiceRecord(
serviceName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- SHORT_TTL - 15_000L,
+ SHORT_TTL /* ttlMillis */,
0 /* servicePriority */,
0 /* serviceWeight */,
TEST_PORT,
- TEST_HOSTNAME))
- doGetReplyWithAnswersTest(questions, knownAnswers, listOf() /* replyAnswers */,
- listOf() /* additionalAnswers */, false /* expectReply */)
+ TEST_CUSTOM_HOST_1_NAME))
+ }
+
+ @Test
+ fun testRestartAnnouncingForHostname() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
+ repository.initWithService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1,
+ setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
+ repository.addServiceAndFinishProbing(TEST_SERVICE_CUSTOM_HOST_ID_1,
+ TEST_SERVICE_CUSTOM_HOST_NO_ADDRESSES)
+ repository.removeService(TEST_CUSTOM_HOST_ID_1)
+
+ val announcementInfos = repository.restartAnnouncingForHostname("TestHost")
+
+ assertEquals(1, announcementInfos.size)
+ val announcementInfo = announcementInfos.get(0)
+ assertEquals(TEST_SERVICE_CUSTOM_HOST_ID_1, announcementInfo.serviceId)
+ val packet = announcementInfo.getPacket(0)
+ assertEquals(0, packet.transactionId)
+ assertEquals(0x8400 /* response, authoritative */, packet.flags)
+ assertEquals(0, packet.questions.size)
+ assertEquals(0, packet.authorityRecords.size)
+ val serviceName = arrayOf("TestService", "_testservice", "_tcp", "local")
+ val serviceType = arrayOf("_testservice", "_tcp", "local")
+ val v4AddrRev = getReverseDnsAddress(TEST_ADDRESSES[0].address)
+ val v6Addr1Rev = getReverseDnsAddress(TEST_ADDRESSES[1].address)
+ val v6Addr2Rev = getReverseDnsAddress(TEST_ADDRESSES[2].address)
+ assertThat(packet.answers).containsExactly(
+ MdnsPointerRecord(
+ serviceType,
+ 0L /* receiptTimeMillis */,
+ // Not a unique name owned by the announcer, so cacheFlush=false
+ false /* cacheFlush */,
+ 4500000L /* ttlMillis */,
+ serviceName),
+ MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ 120000L /* ttlMillis */,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT /* servicePort */,
+ TEST_CUSTOM_HOST_1_NAME),
+ MdnsTextRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ 4500000L /* ttlMillis */,
+ emptyList() /* entries */),
+ MdnsPointerRecord(
+ arrayOf("_services", "_dns-sd", "_udp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 4500000L /* ttlMillis */,
+ serviceType))
+ assertThat(packet.additionalRecords).containsExactly(
+ MdnsNsecRecord(v4AddrRev,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ 120000L /* ttlMillis */,
+ v4AddrRev,
+ intArrayOf(TYPE_PTR)),
+ MdnsNsecRecord(TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ 120000L /* ttlMillis */,
+ TEST_HOSTNAME,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ MdnsNsecRecord(v6Addr1Rev,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ 120000L /* ttlMillis */,
+ v6Addr1Rev,
+ intArrayOf(TYPE_PTR)),
+ MdnsNsecRecord(v6Addr2Rev,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ 120000L /* ttlMillis */,
+ v6Addr2Rev,
+ intArrayOf(TYPE_PTR)),
+ MdnsNsecRecord(serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ 4500000L /* ttlMillis */,
+ serviceName,
+ intArrayOf(TYPE_TXT, TYPE_SRV)))
}
}
@@ -1250,7 +1820,14 @@
): AnnouncementInfo {
updateAddresses(addresses)
serviceInfo.setSubtypes(subtypes)
- addService(serviceId, serviceInfo)
+ return addServiceAndFinishProbing(serviceId, serviceInfo)
+}
+
+private fun MdnsRecordRepository.addServiceAndFinishProbing(
+ serviceId: Int,
+ serviceInfo: NsdServiceInfo
+): AnnouncementInfo {
+ addService(serviceId, serviceInfo, null /* ttl */)
val probingInfo = setServiceProbing(serviceId)
assertNotNull(probingInfo)
return onProbingSucceeded(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
index 9e2933f..9bd0530 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt
@@ -24,21 +24,28 @@
import android.os.Message
import com.android.net.module.util.SharedLog
import com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR
+import com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR
+import com.android.server.connectivity.mdns.MdnsReplySender.getReplyDestination
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
+import java.net.DatagramPacket
import java.net.InetSocketAddress
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
+import kotlin.test.assertEquals
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.any
+import org.mockito.Mockito.anyLong
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.times
import org.mockito.Mockito.verify
private const val TEST_PORT = 12345
@@ -50,8 +57,12 @@
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsReplySenderTest {
private val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ private val otherServiceName = arrayOf("OtherTestService", "_testservice", "_tcp", "local")
private val serviceType = arrayOf("_testservice", "_tcp", "local")
+ private val source = InetSocketAddress(
+ InetAddresses.parseNumericAddress("192.0.2.1"), TEST_PORT)
private val hostname = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
+ private val otherHostname = arrayOf("Android_0F0E0D0C0B0A09080706050403020100", "local")
private val hostAddresses = listOf(
LinkAddress(InetAddresses.parseNumericAddress("192.0.2.111"), 24),
LinkAddress(InetAddresses.parseNumericAddress("2001:db8::111"), 64),
@@ -59,9 +70,12 @@
private val answers = listOf(
MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */, false /* cacheFlush */,
LONG_TTL, serviceName))
+ private val otherAnswers = listOf(
+ MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */, false /* cacheFlush */,
+ LONG_TTL, otherServiceName))
private val additionalAnswers = listOf(
MdnsTextRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */, LONG_TTL,
- listOf() /* entries */),
+ emptyList() /* entries */),
MdnsServiceRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
SHORT_TTL, 0 /* servicePriority */, 0 /* serviceWeight */, TEST_PORT, hostname),
MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
@@ -75,15 +89,30 @@
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 otherAdditionalAnswers = listOf(
+ MdnsTextRecord(otherServiceName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ LONG_TTL, emptyList() /* entries */),
+ MdnsServiceRecord(otherServiceName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, 0 /* servicePriority */, 0 /* serviceWeight */, TEST_PORT,
+ otherHostname),
+ MdnsInetAddressRecord(otherHostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[0].address),
+ MdnsInetAddressRecord(otherHostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[1].address),
+ MdnsInetAddressRecord(otherHostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[2].address),
+ MdnsNsecRecord(otherServiceName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ LONG_TTL, otherServiceName /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+ MdnsNsecRecord(otherHostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, otherHostname /* 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() {
@@ -106,37 +135,180 @@
return future.get(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
}
- private fun sendNow(packet: MdnsPacket, destination: InetSocketAddress):
- Unit = runningOnHandlerAndReturn { replySender.sendNow(packet, destination) }
+ private fun sendNow(sender: MdnsReplySender, packet: MdnsPacket, dest: InetSocketAddress):
+ Unit = runningOnHandlerAndReturn { sender.sendNow(packet, dest) }
- private fun queueReply(reply: MdnsReplyInfo):
- Unit = runningOnHandlerAndReturn { replySender.queueReply(reply) }
+ private fun queueReply(sender: MdnsReplySender, reply: MdnsReplyInfo):
+ Unit = runningOnHandlerAndReturn { sender.queueReply(reply) }
+
+ private fun buildFlags(enableKAS: Boolean): MdnsFeatureFlags {
+ return MdnsFeatureFlags.newBuilder()
+ .setIsKnownAnswerSuppressionEnabled(enableKAS).build()
+ }
+
+ private fun createSender(enableKAS: Boolean): MdnsReplySender =
+ MdnsReplySender(thread.looper, socket, buffer, sharedLog, false /* enableDebugLog */,
+ deps, buildFlags(enableKAS))
@Test
fun testSendNow() {
+ val replySender = createSender(enableKAS = false)
val packet = MdnsPacket(0x8400,
- listOf() /* questions */,
+ emptyList() /* questions */,
answers,
- listOf() /* authorityRecords */,
+ emptyList() /* authorityRecords */,
additionalAnswers)
- sendNow(packet, IPV4_SOCKET_ADDR)
+ sendNow(replySender, packet, IPV4_SOCKET_ADDR)
verify(socket).send(argThat{ it.socketAddress.equals(IPV4_SOCKET_ADDR) })
}
+ private fun verifyMessageQueued(
+ sender: MdnsReplySender,
+ replies: List<MdnsReplyInfo>
+ ): Pair<Handler, Message> {
+ val handlerCaptor = ArgumentCaptor.forClass(Handler::class.java)
+ val messageCaptor = ArgumentCaptor.forClass(Message::class.java)
+ for (reply in replies) {
+ queueReply(sender, reply)
+ verify(deps).sendMessageDelayed(
+ handlerCaptor.capture(), messageCaptor.capture(), eq(reply.sendDelayMs))
+ }
+ return Pair(handlerCaptor.value, messageCaptor.value)
+ }
+
+ private fun verifyReplySent(
+ realHandler: Handler,
+ delayMessage: Message,
+ remainingAnswers: List<MdnsRecord>
+ ) {
+ val datagramPacketCaptor = ArgumentCaptor.forClass(DatagramPacket::class.java)
+ realHandler.sendMessage(delayMessage)
+ verify(socket, timeout(DEFAULT_TIMEOUT_MS)).send(datagramPacketCaptor.capture())
+
+ val dPacket = datagramPacketCaptor.value
+ val mdnsPacket = MdnsPacket.parse(MdnsPacketReader(
+ dPacket.data, dPacket.length, buildFlags(enableKAS = false)))
+ assertEquals(mdnsPacket.answers.toSet(), remainingAnswers.toSet())
+ }
+
@Test
fun testQueueReply() {
+ val replySender = createSender(enableKAS = false)
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))
+ IPV4_SOCKET_ADDR, source, emptyList())
+ val (handler, message) = verifyMessageQueued(replySender, listOf(reply))
+ verifyReplySent(handler, message, answers)
+ }
- 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)
- })
+ @Test
+ fun testQueueReply_KnownAnswerSuppressionEnabled() {
+ val replySender = createSender(enableKAS = true)
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 20L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, emptyList())
+ val (handler, message) = verifyMessageQueued(replySender, listOf(reply))
+ verifyReplySent(handler, message, answers)
+ }
+
+ @Test
+ fun testQueueReply_MultiplePacket() {
+ val replySender = createSender(enableKAS = true)
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 400L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, emptyList())
+ verifyMessageQueued(replySender, listOf(reply))
+
+ // Receive a known-answer packet and verify no message queued.
+ val knownAnswersReply = MdnsReplyInfo(emptyList(), emptyList(), 0L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, answers)
+ queueReply(replySender, knownAnswersReply)
+ verify(deps, times(1)).sendMessageDelayed(any(), any(), anyLong())
+ }
+
+ @Test
+ fun testQueueReply_MultiplePacket_LostSubsequentPacket() {
+ val replySender = createSender(enableKAS = true)
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 400L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, emptyList())
+ val (handler, message) = verifyMessageQueued(replySender, listOf(reply))
+
+ // No subsequent packets
+ verifyReplySent(handler, message, answers)
+ }
+
+ @Test
+ fun testQueueReply_MultiplePacket_OtherKnownAnswer() {
+ val replySender = createSender(enableKAS = true)
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 400L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, emptyList())
+ // Other known-answer service
+ val otherKnownAnswersReply = MdnsReplyInfo(emptyList(), emptyList(), 0L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, otherAnswers)
+ val (handler, message) = verifyMessageQueued(
+ replySender, listOf(reply, otherKnownAnswersReply))
+ verifyReplySent(handler, message, answers)
+ }
+
+ @Test
+ fun testQueueReply_MultiplePacket_TwoKnownAnswerPackets() {
+ val replySender = createSender(enableKAS = true)
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 400L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, emptyList())
+ val firstKnownAnswerReply = MdnsReplyInfo(emptyList(), emptyList(), 401L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, otherAnswers)
+ verifyMessageQueued(replySender, listOf(reply, firstKnownAnswerReply))
+
+ // Second known-answer service
+ val secondKnownAnswerReply = MdnsReplyInfo(emptyList(), emptyList(), 0L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, answers)
+ queueReply(replySender, secondKnownAnswerReply)
+
+ // Verify that no reply is queued, as all answers are known.
+ verify(deps, times(2)).sendMessageDelayed(any(), any(), anyLong())
+ }
+
+ @Test
+ fun testQueueReply_MultiplePacket_LostSecondaryPacket() {
+ val replySender = createSender(enableKAS = true)
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 400L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, emptyList())
+ val firstKnownAnswerReply = MdnsReplyInfo(emptyList(), emptyList(), 401L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, otherAnswers)
+ val (handler, message) = verifyMessageQueued(
+ replySender, listOf(reply, firstKnownAnswerReply))
+
+ // Second known-answer service lost
+ verifyReplySent(handler, message, answers)
+ }
+
+ @Test
+ fun testQueueReply_MultiplePacket_WithMultipleQuestions() {
+ val replySender = createSender(enableKAS = true)
+ val twoAnswers = listOf(
+ MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */, false /* cacheFlush */,
+ LONG_TTL, serviceName),
+ MdnsServiceRecord(otherServiceName, 0L /* receiptTimeMillis */,
+ true /* cacheFlush */, SHORT_TTL, 0 /* servicePriority */,
+ 0 /* serviceWeight */, TEST_PORT, otherHostname))
+ val reply = MdnsReplyInfo(twoAnswers, additionalAnswers, 400L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR, source, emptyList())
+ val knownAnswersReply = MdnsReplyInfo(otherAnswers, otherAdditionalAnswers,
+ 20L /* sendDelayMs */, IPV4_SOCKET_ADDR, source, answers)
+ val (handler, message) = verifyMessageQueued(replySender, listOf(reply, knownAnswersReply))
+
+ val remainingAnswers = listOf(
+ MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */, false /* cacheFlush */,
+ LONG_TTL, otherServiceName),
+ MdnsServiceRecord(otherServiceName, 0L /* receiptTimeMillis */,
+ true /* cacheFlush */, SHORT_TTL, 0 /* servicePriority */,
+ 0 /* serviceWeight */, TEST_PORT, otherHostname))
+ verifyReplySent(handler, message, remainingAnswers)
+ }
+
+ @Test
+ fun testGetReplyDestination() {
+ assertEquals(IPV4_SOCKET_ADDR, getReplyDestination(IPV4_SOCKET_ADDR, IPV4_SOCKET_ADDR))
+ assertEquals(IPV6_SOCKET_ADDR, getReplyDestination(IPV6_SOCKET_ADDR, IPV6_SOCKET_ADDR))
+ assertEquals(IPV4_SOCKET_ADDR, getReplyDestination(source, IPV4_SOCKET_ADDR))
+ assertEquals(IPV6_SOCKET_ADDR, getReplyDestination(source, IPV6_SOCKET_ADDR))
+ assertEquals(source, getReplyDestination(source, source))
}
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
index e7d7a98..4ce8ba6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
@@ -35,6 +35,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.time.Instant;
import java.util.List;
import java.util.Map;
@@ -53,7 +54,8 @@
"192.168.1.1",
"2001::1",
List.of("vn=Google Inc.", "mn=Google Nest Hub Max"),
- /* textEntries= */ null);
+ /* textEntries= */ null,
+ INTERFACE_INDEX_UNSPECIFIED);
assertTrue(info.getAttributeByKey("vn").equals("Google Inc."));
assertTrue(info.getAttributeByKey("mn").equals("Google Nest Hub Max"));
@@ -72,7 +74,8 @@
"2001::1",
/* textStrings= */ null,
List.of(MdnsServiceInfo.TextEntry.fromString("vn=Google Inc."),
- MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")));
+ MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")),
+ INTERFACE_INDEX_UNSPECIFIED);
assertTrue(info.getAttributeByKey("vn").equals("Google Inc."));
assertTrue(info.getAttributeByKey("mn").equals("Google Nest Hub Max"));
@@ -92,7 +95,8 @@
List.of("vn=Alphabet Inc.", "mn=Google Nest Hub Max", "id=12345"),
List.of(
MdnsServiceInfo.TextEntry.fromString("vn=Google Inc."),
- MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")));
+ MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")),
+ INTERFACE_INDEX_UNSPECIFIED);
assertEquals(Map.of("vn", "Google Inc.", "mn", "Google Nest Hub Max"),
info.getAttributes());
@@ -112,7 +116,8 @@
List.of("vn=Alphabet Inc.", "mn=Google Nest Hub Max", "id=12345"),
List.of(MdnsServiceInfo.TextEntry.fromString("vn=Google Inc."),
MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max"),
- MdnsServiceInfo.TextEntry.fromString("mn=Google WiFi Router")));
+ MdnsServiceInfo.TextEntry.fromString("mn=Google WiFi Router")),
+ INTERFACE_INDEX_UNSPECIFIED);
assertEquals(Map.of("vn", "Google Inc.", "mn", "Google Nest Hub Max"),
info.getAttributes());
@@ -130,7 +135,8 @@
"192.168.1.1",
"2001::1",
List.of("KEY=Value"),
- /* textEntries= */ null);
+ /* textEntries= */ null,
+ INTERFACE_INDEX_UNSPECIFIED);
assertEquals("Value", info.getAttributeByKey("key"));
assertEquals("Value", info.getAttributeByKey("KEY"));
@@ -149,7 +155,9 @@
12345,
"192.168.1.1",
"2001::1",
- List.of());
+ List.of(),
+ /* textEntries= */ null,
+ INTERFACE_INDEX_UNSPECIFIED);
assertEquals(info.getInterfaceIndex(), INTERFACE_INDEX_UNSPECIFIED);
}
@@ -202,7 +210,8 @@
List.of(),
/* textEntries= */ null,
/* interfaceIndex= */ 20,
- network);
+ network,
+ Instant.MAX /* expirationTime */);
assertEquals(network, info2.getNetwork());
}
@@ -225,7 +234,8 @@
MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max"),
MdnsServiceInfo.TextEntry.fromString("test=")),
20 /* interfaceIndex */,
- new Network(123));
+ new Network(123),
+ Instant.MAX /* expirationTime */);
beforeParcel.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index 7a2e4bf..44fa55c 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -16,7 +16,13 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.ACTIVE_QUERY_MODE;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.MdnsServiceTypeClient.EVENT_START_QUERYTASK;
+import static com.android.server.connectivity.mdns.QueryTaskConfig.INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
+import static com.android.server.connectivity.mdns.QueryTaskConfig.MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS;
+import static com.android.server.connectivity.mdns.QueryTaskConfig.TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertArrayEquals;
@@ -32,11 +38,13 @@
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -110,8 +118,6 @@
@Mock
private MdnsServiceBrowserListener mockListenerTwo;
@Mock
- private MdnsPacketWriter mockPacketWriter;
- @Mock
private MdnsMultinetworkSocketClient mockSocketClient;
@Mock
private Network mockNetwork;
@@ -138,6 +144,7 @@
private long latestDelayMs = 0;
private Message delayMessage = null;
private Handler realHandler = null;
+ private MdnsFeatureFlags featureFlags = MdnsFeatureFlags.newBuilder().build();
@Before
@SuppressWarnings("DoNotMock")
@@ -145,8 +152,8 @@
MockitoAnnotations.initMocks(this);
doReturn(TEST_ELAPSED_REALTIME).when(mockDecoderClock).elapsedRealtime();
- expectedIPv4Packets = new DatagramPacket[16];
- expectedIPv6Packets = new DatagramPacket[16];
+ expectedIPv4Packets = new DatagramPacket[24];
+ expectedIPv6Packets = new DatagramPacket[24];
socketKey = new SocketKey(mockNetwork, INTERFACE_INDEX);
for (int i = 0; i < expectedIPv4Packets.length; ++i) {
@@ -155,41 +162,59 @@
expectedIPv6Packets[i] = new DatagramPacket(buf, 0 /* offset */, 5 /* length */,
MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
}
- when(mockPacketWriter.getPacket(IPV4_ADDRESS))
- .thenReturn(expectedIPv4Packets[0])
- .thenReturn(expectedIPv4Packets[1])
- .thenReturn(expectedIPv4Packets[2])
- .thenReturn(expectedIPv4Packets[3])
- .thenReturn(expectedIPv4Packets[4])
- .thenReturn(expectedIPv4Packets[5])
- .thenReturn(expectedIPv4Packets[6])
- .thenReturn(expectedIPv4Packets[7])
- .thenReturn(expectedIPv4Packets[8])
- .thenReturn(expectedIPv4Packets[9])
- .thenReturn(expectedIPv4Packets[10])
- .thenReturn(expectedIPv4Packets[11])
- .thenReturn(expectedIPv4Packets[12])
- .thenReturn(expectedIPv4Packets[13])
- .thenReturn(expectedIPv4Packets[14])
- .thenReturn(expectedIPv4Packets[15]);
+ when(mockDeps.getDatagramPacketsFromMdnsPacket(
+ any(), any(MdnsPacket.class), eq(IPV4_ADDRESS), anyBoolean()))
+ .thenReturn(List.of(expectedIPv4Packets[0]))
+ .thenReturn(List.of(expectedIPv4Packets[1]))
+ .thenReturn(List.of(expectedIPv4Packets[2]))
+ .thenReturn(List.of(expectedIPv4Packets[3]))
+ .thenReturn(List.of(expectedIPv4Packets[4]))
+ .thenReturn(List.of(expectedIPv4Packets[5]))
+ .thenReturn(List.of(expectedIPv4Packets[6]))
+ .thenReturn(List.of(expectedIPv4Packets[7]))
+ .thenReturn(List.of(expectedIPv4Packets[8]))
+ .thenReturn(List.of(expectedIPv4Packets[9]))
+ .thenReturn(List.of(expectedIPv4Packets[10]))
+ .thenReturn(List.of(expectedIPv4Packets[11]))
+ .thenReturn(List.of(expectedIPv4Packets[12]))
+ .thenReturn(List.of(expectedIPv4Packets[13]))
+ .thenReturn(List.of(expectedIPv4Packets[14]))
+ .thenReturn(List.of(expectedIPv4Packets[15]))
+ .thenReturn(List.of(expectedIPv4Packets[16]))
+ .thenReturn(List.of(expectedIPv4Packets[17]))
+ .thenReturn(List.of(expectedIPv4Packets[18]))
+ .thenReturn(List.of(expectedIPv4Packets[19]))
+ .thenReturn(List.of(expectedIPv4Packets[20]))
+ .thenReturn(List.of(expectedIPv4Packets[21]))
+ .thenReturn(List.of(expectedIPv4Packets[22]))
+ .thenReturn(List.of(expectedIPv4Packets[23]));
- when(mockPacketWriter.getPacket(IPV6_ADDRESS))
- .thenReturn(expectedIPv6Packets[0])
- .thenReturn(expectedIPv6Packets[1])
- .thenReturn(expectedIPv6Packets[2])
- .thenReturn(expectedIPv6Packets[3])
- .thenReturn(expectedIPv6Packets[4])
- .thenReturn(expectedIPv6Packets[5])
- .thenReturn(expectedIPv6Packets[6])
- .thenReturn(expectedIPv6Packets[7])
- .thenReturn(expectedIPv6Packets[8])
- .thenReturn(expectedIPv6Packets[9])
- .thenReturn(expectedIPv6Packets[10])
- .thenReturn(expectedIPv6Packets[11])
- .thenReturn(expectedIPv6Packets[12])
- .thenReturn(expectedIPv6Packets[13])
- .thenReturn(expectedIPv6Packets[14])
- .thenReturn(expectedIPv6Packets[15]);
+ when(mockDeps.getDatagramPacketsFromMdnsPacket(
+ any(), any(MdnsPacket.class), eq(IPV6_ADDRESS), anyBoolean()))
+ .thenReturn(List.of(expectedIPv6Packets[0]))
+ .thenReturn(List.of(expectedIPv6Packets[1]))
+ .thenReturn(List.of(expectedIPv6Packets[2]))
+ .thenReturn(List.of(expectedIPv6Packets[3]))
+ .thenReturn(List.of(expectedIPv6Packets[4]))
+ .thenReturn(List.of(expectedIPv6Packets[5]))
+ .thenReturn(List.of(expectedIPv6Packets[6]))
+ .thenReturn(List.of(expectedIPv6Packets[7]))
+ .thenReturn(List.of(expectedIPv6Packets[8]))
+ .thenReturn(List.of(expectedIPv6Packets[9]))
+ .thenReturn(List.of(expectedIPv6Packets[10]))
+ .thenReturn(List.of(expectedIPv6Packets[11]))
+ .thenReturn(List.of(expectedIPv6Packets[12]))
+ .thenReturn(List.of(expectedIPv6Packets[13]))
+ .thenReturn(List.of(expectedIPv6Packets[14]))
+ .thenReturn(List.of(expectedIPv6Packets[15]))
+ .thenReturn(List.of(expectedIPv6Packets[16]))
+ .thenReturn(List.of(expectedIPv6Packets[17]))
+ .thenReturn(List.of(expectedIPv6Packets[18]))
+ .thenReturn(List.of(expectedIPv6Packets[19]))
+ .thenReturn(List.of(expectedIPv6Packets[20]))
+ .thenReturn(List.of(expectedIPv6Packets[21]))
+ .thenReturn(List.of(expectedIPv6Packets[22]))
+ .thenReturn(List.of(expectedIPv6Packets[23]));
thread = new HandlerThread("MdnsServiceTypeClientTests");
thread.start();
@@ -219,15 +244,13 @@
return true;
}).when(mockDeps).sendMessage(any(Handler.class), any(Message.class));
- client =
- new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache) {
- @Override
- MdnsPacketWriter createMdnsPacketWriter() {
- return mockPacketWriter;
- }
- };
+ client = makeMdnsServiceTypeClient();
+ }
+
+ private MdnsServiceTypeClient makeMdnsServiceTypeClient() {
+ return new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
+ mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
+ serviceCache, featureFlags);
}
@After
@@ -267,8 +290,8 @@
@Test
public void sendQueries_activeScanMode() {
- MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(false).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
startSendAndReceive(mockListenerOne, searchOptions);
// Always try to remove the task.
verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
@@ -319,8 +342,8 @@
@Test
public void sendQueries_reentry_activeScanMode() {
- MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(false).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
startSendAndReceive(mockListenerOne, searchOptions);
// Always try to remove the task.
verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
@@ -333,7 +356,7 @@
MdnsSearchOptions.newBuilder()
.addSubtype(SUBTYPE)
.addSubtype("_subtype2")
- .setIsPassiveMode(false)
+ .setQueryMode(ACTIVE_QUERY_MODE)
.build();
startSendAndReceive(mockListenerOne, searchOptions);
// The previous scheduled task should be canceled.
@@ -353,8 +376,8 @@
@Test
public void sendQueries_passiveScanMode() {
- MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(true).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(PASSIVE_QUERY_MODE).build();
startSendAndReceive(mockListenerOne, searchOptions);
// Always try to remove the task.
verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
@@ -380,8 +403,10 @@
@Test
public void sendQueries_activeScanWithQueryBackoff() {
MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(
- false).setNumOfQueriesBeforeBackoff(11).build();
+ MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE)
+ .setQueryMode(ACTIVE_QUERY_MODE)
+ .setNumOfQueriesBeforeBackoff(11).build();
startSendAndReceive(mockListenerOne, searchOptions);
// Always try to remove the task.
verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
@@ -439,8 +464,10 @@
@Test
public void sendQueries_passiveScanWithQueryBackoff() {
MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(
- true).setNumOfQueriesBeforeBackoff(3).build();
+ MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE)
+ .setQueryMode(PASSIVE_QUERY_MODE)
+ .setNumOfQueriesBeforeBackoff(3).build();
startSendAndReceive(mockListenerOne, searchOptions);
// Always try to remove the task.
verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
@@ -497,8 +524,8 @@
@Test
public void sendQueries_reentry_passiveScanMode() {
- MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(true).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(PASSIVE_QUERY_MODE).build();
startSendAndReceive(mockListenerOne, searchOptions);
// Always try to remove the task.
verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
@@ -511,7 +538,7 @@
MdnsSearchOptions.newBuilder()
.addSubtype(SUBTYPE)
.addSubtype("_subtype2")
- .setIsPassiveMode(true)
+ .setQueryMode(PASSIVE_QUERY_MODE)
.build();
startSendAndReceive(mockListenerOne, searchOptions);
// The previous scheduled task should be canceled.
@@ -533,16 +560,15 @@
@Ignore("MdnsConfigs is not configurable currently.")
public void testQueryTaskConfig_alwaysAskForUnicastResponse() {
//MdnsConfigsFlagsImpl.alwaysAskForUnicastResponseInEachBurst.override(true);
- MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(false).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
QueryTaskConfig config = new QueryTaskConfig(
- searchOptions.getSubtypes(), searchOptions.isPassiveMode(),
+ searchOptions.getQueryMode(),
false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
socketKey);
// This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse);
- assertEquals(config.subtypes, searchOptions.getSubtypes());
assertEquals(config.transactionId, 1);
// For the rest of queries in this burst, we will NOT ask for unicast response.
@@ -550,7 +576,6 @@
int oldTransactionId = config.transactionId;
config = config.getConfigForNextRun();
assertFalse(config.expectUnicastResponse);
- assertEquals(config.subtypes, searchOptions.getSubtypes());
assertEquals(config.transactionId, oldTransactionId + 1);
}
@@ -558,22 +583,20 @@
int oldTransactionId = config.transactionId;
config = config.getConfigForNextRun();
assertTrue(config.expectUnicastResponse);
- assertEquals(config.subtypes, searchOptions.getSubtypes());
assertEquals(config.transactionId, oldTransactionId + 1);
}
@Test
public void testQueryTaskConfig_askForUnicastInFirstQuery() {
- MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(false).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(ACTIVE_QUERY_MODE).build();
QueryTaskConfig config = new QueryTaskConfig(
- searchOptions.getSubtypes(), searchOptions.isPassiveMode(),
+ searchOptions.getQueryMode(),
false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
socketKey);
// This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse);
- assertEquals(config.subtypes, searchOptions.getSubtypes());
assertEquals(config.transactionId, 1);
// For the rest of queries in this burst, we will NOT ask for unicast response.
@@ -581,7 +604,6 @@
int oldTransactionId = config.transactionId;
config = config.getConfigForNextRun();
assertFalse(config.expectUnicastResponse);
- assertEquals(config.subtypes, searchOptions.getSubtypes());
assertEquals(config.transactionId, oldTransactionId + 1);
}
@@ -589,14 +611,13 @@
int oldTransactionId = config.transactionId;
config = config.getConfigForNextRun();
assertFalse(config.expectUnicastResponse);
- assertEquals(config.subtypes, searchOptions.getSubtypes());
assertEquals(config.transactionId, oldTransactionId + 1);
}
@Test
public void testIfPreviousTaskIsCanceledWhenNewSessionStarts() {
- MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(true).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(PASSIVE_QUERY_MODE).build();
startSendAndReceive(mockListenerOne, searchOptions);
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
@@ -605,7 +626,7 @@
MdnsSearchOptions.newBuilder()
.addSubtype(SUBTYPE)
.addSubtype("_subtype2")
- .setIsPassiveMode(true)
+ .setQueryMode(PASSIVE_QUERY_MODE)
.build();
startSendAndReceive(mockListenerOne, searchOptions);
@@ -624,8 +645,8 @@
@Ignore("MdnsConfigs is not configurable currently.")
public void testIfPreviousTaskIsCanceledWhenSessionStops() {
//MdnsConfigsFlagsImpl.shouldCancelScanTaskWhenFutureIsNull.override(true);
- MdnsSearchOptions searchOptions =
- MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE).setIsPassiveMode(true).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(PASSIVE_QUERY_MODE).build();
startSendAndReceive(mockListenerOne, searchOptions);
// Change the sutypes and start a new session.
stopSendAndReceive(mockListenerOne);
@@ -667,6 +688,81 @@
any(), any(), eq(MdnsConfigs.timeBetweenQueriesInBurstMs()));
}
+ @Test
+ public void testCombinedSubtypesQueriedWithMultipleListeners() throws Exception {
+ final MdnsSearchOptions searchOptions1 = MdnsSearchOptions.newBuilder()
+ .addSubtype("subtype1").build();
+ final MdnsSearchOptions searchOptions2 = MdnsSearchOptions.newBuilder()
+ .addSubtype("subtype2").build();
+ doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
+ any(), any(MdnsPacket.class), any(InetSocketAddress.class), anyBoolean());
+ startSendAndReceive(mockListenerOne, searchOptions1);
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+
+ InOrder inOrder = inOrder(mockListenerOne, mockSocketClient, mockDeps);
+
+ // Verify the query asks for subtype1
+ final ArgumentCaptor<List<DatagramPacket>> subtype1QueryCaptor =
+ ArgumentCaptor.forClass(List.class);
+ // Send twice for IPv4 and IPv6
+ inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
+ subtype1QueryCaptor.capture(),
+ eq(socketKey), eq(false));
+
+ final MdnsPacket subtype1Query = MdnsPacket.parse(
+ new MdnsPacketReader(subtype1QueryCaptor.getValue().get(0)));
+
+ assertEquals(2, subtype1Query.questions.size());
+ assertTrue(hasQuestion(subtype1Query, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertTrue(hasQuestion(subtype1Query, MdnsRecord.TYPE_PTR,
+ getServiceTypeWithSubtype("_subtype1")));
+
+ // Add subtype2
+ startSendAndReceive(mockListenerTwo, searchOptions2);
+ inOrder.verify(mockDeps).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+
+ final ArgumentCaptor<List<DatagramPacket>> combinedSubtypesQueryCaptor =
+ ArgumentCaptor.forClass(List.class);
+ inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
+ combinedSubtypesQueryCaptor.capture(),
+ eq(socketKey), eq(false));
+ // The next query must have been scheduled
+ inOrder.verify(mockDeps).sendMessageDelayed(any(), any(), anyLong());
+
+ final MdnsPacket combinedSubtypesQuery = MdnsPacket.parse(
+ new MdnsPacketReader(combinedSubtypesQueryCaptor.getValue().get(0)));
+
+ assertEquals(3, combinedSubtypesQuery.questions.size());
+ assertTrue(hasQuestion(combinedSubtypesQuery, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertTrue(hasQuestion(combinedSubtypesQuery, MdnsRecord.TYPE_PTR,
+ getServiceTypeWithSubtype("_subtype1")));
+ assertTrue(hasQuestion(combinedSubtypesQuery, MdnsRecord.TYPE_PTR,
+ getServiceTypeWithSubtype("_subtype2")));
+
+ // Remove subtype1
+ stopSendAndReceive(mockListenerOne);
+
+ // Queries are not rescheduled, but the next query is affected
+ dispatchMessage();
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+
+ final ArgumentCaptor<List<DatagramPacket>> subtype2QueryCaptor =
+ ArgumentCaptor.forClass(List.class);
+ // Send twice for IPv4 and IPv6
+ inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
+ subtype2QueryCaptor.capture(),
+ eq(socketKey), eq(false));
+
+ final MdnsPacket subtype2Query = MdnsPacket.parse(
+ new MdnsPacketReader(subtype2QueryCaptor.getValue().get(0)));
+
+ assertEquals(2, subtype2Query.questions.size());
+ assertTrue(hasQuestion(subtype2Query, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertTrue(hasQuestion(subtype2Query, MdnsRecord.TYPE_PTR,
+ getServiceTypeWithSubtype("_subtype2")));
+ }
+
private static void verifyServiceInfo(MdnsServiceInfo serviceInfo, String serviceName,
String[] serviceType, List<String> ipv4Addresses, List<String> ipv6Addresses, int port,
List<String> subTypes, Map<String, String> attributes, SocketKey socketKey) {
@@ -919,15 +1015,6 @@
public void processResponse_searchOptionsEnableServiceRemoval_shouldRemove()
throws Exception {
final String serviceInstanceName = "service-instance-1";
- client =
- new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache) {
- @Override
- MdnsPacketWriter createMdnsPacketWriter() {
- return mockPacketWriter;
- }
- };
MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
.setRemoveExpiredService(true)
.setNumOfQueriesBeforeBackoff(Integer.MAX_VALUE)
@@ -965,15 +1052,6 @@
public void processResponse_searchOptionsNotEnableServiceRemoval_shouldNotRemove()
throws Exception {
final String serviceInstanceName = "service-instance-1";
- client =
- new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache) {
- @Override
- MdnsPacketWriter createMdnsPacketWriter() {
- return mockPacketWriter;
- }
- };
startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
@@ -999,15 +1077,6 @@
throws Exception {
//MdnsConfigsFlagsImpl.removeServiceAfterTtlExpires.override(true);
final String serviceInstanceName = "service-instance-1";
- client =
- new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache) {
- @Override
- MdnsPacketWriter createMdnsPacketWriter() {
- return mockPacketWriter;
- }
- };
startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
@@ -1122,24 +1191,27 @@
@Test
public void testProcessResponse_Resolve() throws Exception {
- client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache);
-
final String instanceName = "service-instance";
final String[] hostname = new String[] { "testhost "};
final String ipV4Address = "192.0.2.0";
final String ipV6Address = "2001:db8::";
- final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
+ final MdnsSearchOptions resolveOptions1 = MdnsSearchOptions.newBuilder()
+ .setResolveInstanceName(instanceName).build();
+ final MdnsSearchOptions resolveOptions2 = MdnsSearchOptions.newBuilder()
.setResolveInstanceName(instanceName).build();
- startSendAndReceive(mockListenerOne, resolveOptions);
+ doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
+ any(), any(MdnsPacket.class), any(InetSocketAddress.class), anyBoolean());
+
+ startSendAndReceive(mockListenerOne, resolveOptions1);
+ startSendAndReceive(mockListenerTwo, resolveOptions2);
+ // No need to verify order for both listeners; and order is not guaranteed between them
InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
// Verify a query for SRV/TXT was sent, but no PTR query
- final ArgumentCaptor<DatagramPacket> srvTxtQueryCaptor =
- ArgumentCaptor.forClass(DatagramPacket.class);
+ final ArgumentCaptor<List<DatagramPacket>> srvTxtQueryCaptor =
+ ArgumentCaptor.forClass(List.class);
currentThreadExecutor.getAndClearLastScheduledRunnable().run();
// Send twice for IPv4 and IPv6
inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
@@ -1147,13 +1219,19 @@
eq(socketKey), eq(false));
verify(mockDeps, times(1)).sendMessage(any(), any(Message.class));
assertNotNull(delayMessage);
+ inOrder.verify(mockListenerOne).onDiscoveryQuerySent(any(), anyInt());
+ verify(mockListenerTwo).onDiscoveryQuerySent(any(), anyInt());
final MdnsPacket srvTxtQueryPacket = MdnsPacket.parse(
- new MdnsPacketReader(srvTxtQueryCaptor.getValue()));
+ new MdnsPacketReader(srvTxtQueryCaptor.getValue().get(0)));
final String[] serviceName = getTestServiceName(instanceName);
+ assertEquals(1, srvTxtQueryPacket.questions.size());
assertFalse(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_PTR));
assertTrue(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_ANY, serviceName));
+ assertEquals(0, srvTxtQueryPacket.answers.size());
+ assertEquals(0, srvTxtQueryPacket.authorityRecords.size());
+ assertEquals(0, srvTxtQueryPacket.additionalRecords.size());
// Process a response with SRV+TXT
final MdnsPacket srvTxtResponse = new MdnsPacket(
@@ -1170,20 +1248,31 @@
Collections.emptyList() /* additionalRecords */);
processResponse(srvTxtResponse, socketKey);
+ inOrder.verify(mockListenerOne).onServiceNameDiscovered(
+ matchServiceName(instanceName), eq(false) /* isServiceFromCache */);
+ verify(mockListenerTwo).onServiceNameDiscovered(
+ matchServiceName(instanceName), eq(false) /* isServiceFromCache */);
// Expect a query for A/AAAA
dispatchMessage();
- final ArgumentCaptor<DatagramPacket> addressQueryCaptor =
- ArgumentCaptor.forClass(DatagramPacket.class);
+ final ArgumentCaptor<List<DatagramPacket>> addressQueryCaptor =
+ ArgumentCaptor.forClass(List.class);
currentThreadExecutor.getAndClearLastScheduledRunnable().run();
inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
addressQueryCaptor.capture(),
eq(socketKey), eq(false));
+ inOrder.verify(mockListenerOne).onDiscoveryQuerySent(any(), anyInt());
+ // onDiscoveryQuerySent was called 2 times in total
+ verify(mockListenerTwo, times(2)).onDiscoveryQuerySent(any(), anyInt());
final MdnsPacket addressQueryPacket = MdnsPacket.parse(
- new MdnsPacketReader(addressQueryCaptor.getValue()));
+ new MdnsPacketReader(addressQueryCaptor.getValue().get(0)));
+ assertEquals(2, addressQueryPacket.questions.size());
assertTrue(hasQuestion(addressQueryPacket, MdnsRecord.TYPE_A, hostname));
assertTrue(hasQuestion(addressQueryPacket, MdnsRecord.TYPE_AAAA, hostname));
+ assertEquals(0, addressQueryPacket.answers.size());
+ assertEquals(0, addressQueryPacket.authorityRecords.size());
+ assertEquals(0, addressQueryPacket.additionalRecords.size());
// Process a response with address records
final MdnsPacket addressResponse = new MdnsPacket(
@@ -1200,10 +1289,12 @@
Collections.emptyList() /* additionalRecords */);
inOrder.verify(mockListenerOne, never()).onServiceNameDiscovered(any(), anyBoolean());
+ verifyNoMoreInteractions(mockListenerTwo);
processResponse(addressResponse, socketKey);
inOrder.verify(mockListenerOne).onServiceFound(
serviceInfoCaptor.capture(), eq(false) /* isServiceFromCache */);
+ verify(mockListenerTwo).onServiceFound(any(), anyBoolean());
verifyServiceInfo(serviceInfoCaptor.getValue(),
instanceName,
SERVICE_TYPE_LABELS,
@@ -1217,10 +1308,6 @@
@Test
public void testRenewTxtSrvInResolve() throws Exception {
- client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache);
-
final String instanceName = "service-instance";
final String[] hostname = new String[] { "testhost "};
final String ipV4Address = "192.0.2.0";
@@ -1229,12 +1316,15 @@
final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
.setResolveInstanceName(instanceName).build();
+ doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
+ any(), any(MdnsPacket.class), any(InetSocketAddress.class), anyBoolean());
+
startSendAndReceive(mockListenerOne, resolveOptions);
InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
// Get the query for SRV/TXT
- final ArgumentCaptor<DatagramPacket> srvTxtQueryCaptor =
- ArgumentCaptor.forClass(DatagramPacket.class);
+ final ArgumentCaptor<List<DatagramPacket>> srvTxtQueryCaptor =
+ ArgumentCaptor.forClass(List.class);
currentThreadExecutor.getAndClearLastScheduledRunnable().run();
// Send twice for IPv4 and IPv6
inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
@@ -1244,7 +1334,7 @@
assertNotNull(delayMessage);
final MdnsPacket srvTxtQueryPacket = MdnsPacket.parse(
- new MdnsPacketReader(srvTxtQueryCaptor.getValue()));
+ new MdnsPacketReader(srvTxtQueryCaptor.getValue().get(0)));
final String[] serviceName = getTestServiceName(instanceName);
assertTrue(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_ANY, serviceName));
@@ -1288,8 +1378,8 @@
currentThreadExecutor.getAndClearLastScheduledRunnable().run();
// Expect a renewal query
- final ArgumentCaptor<DatagramPacket> renewalQueryCaptor =
- ArgumentCaptor.forClass(DatagramPacket.class);
+ final ArgumentCaptor<List<DatagramPacket>> renewalQueryCaptor =
+ ArgumentCaptor.forClass(List.class);
// Second and later sends are sent as "expect multicast response" queries
inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
renewalQueryCaptor.capture(),
@@ -1298,7 +1388,7 @@
assertNotNull(delayMessage);
inOrder.verify(mockListenerOne).onDiscoveryQuerySent(any(), anyInt());
final MdnsPacket renewalPacket = MdnsPacket.parse(
- new MdnsPacketReader(renewalQueryCaptor.getValue()));
+ new MdnsPacketReader(renewalQueryCaptor.getValue().get(0)));
assertTrue(hasQuestion(renewalPacket, MdnsRecord.TYPE_ANY, serviceName));
inOrder.verifyNoMoreInteractions();
@@ -1333,10 +1423,6 @@
@Test
public void testProcessResponse_ResolveExcludesOtherServices() {
- client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache);
-
final String requestedInstance = "instance1";
final String otherInstance = "instance2";
final String ipV4Address = "192.0.2.0";
@@ -1403,10 +1489,6 @@
@Test
public void testProcessResponse_SubtypeDiscoveryLimitedToSubtype() {
- client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache);
-
final String matchingInstance = "instance1";
final String subtype = "_subtype";
final String otherInstance = "instance2";
@@ -1492,11 +1574,88 @@
}
@Test
- public void testNotifySocketDestroyed() throws Exception {
- client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
- mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache);
+ public void testProcessResponse_SubtypeChange() {
+ final String matchingInstance = "instance1";
+ final String subtype = "_subtype";
+ final String ipV4Address = "192.0.2.0";
+ final String ipV6Address = "2001:db8::";
+ final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
+ .addSubtype("othersub").build();
+
+ startSendAndReceive(mockListenerOne, options);
+
+ // Complete response from instanceName
+ final MdnsPacket packetWithoutSubtype = createResponse(
+ matchingInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap() /* textAttributes */, TEST_TTL);
+ final MdnsPointerRecord originalPtr = (MdnsPointerRecord) CollectionUtils.findFirst(
+ packetWithoutSubtype.answers, r -> r instanceof MdnsPointerRecord);
+
+ // Add a subtype PTR record
+ final ArrayList<MdnsRecord> newAnswers = new ArrayList<>(packetWithoutSubtype.answers);
+ newAnswers.add(new MdnsPointerRecord(
+ // PTR should be _subtype._sub._type._tcp.local -> instance1._type._tcp.local
+ Stream.concat(Stream.of(subtype, "_sub"), Arrays.stream(SERVICE_TYPE_LABELS))
+ .toArray(String[]::new),
+ originalPtr.getReceiptTime(), originalPtr.getCacheFlush(), originalPtr.getTtl(),
+ originalPtr.getPointer()));
+ processResponse(new MdnsPacket(
+ packetWithoutSubtype.flags,
+ packetWithoutSubtype.questions,
+ newAnswers,
+ packetWithoutSubtype.authorityRecords,
+ packetWithoutSubtype.additionalRecords), socketKey);
+
+ // The subtype does not match
+ final InOrder inOrder = inOrder(mockListenerOne);
+ inOrder.verify(mockListenerOne, never()).onServiceNameDiscovered(any(), anyBoolean());
+
+ // Add another matching subtype
+ newAnswers.add(new MdnsPointerRecord(
+ // PTR should be _subtype._sub._type._tcp.local -> instance1._type._tcp.local
+ Stream.concat(Stream.of("_othersub", "_sub"), Arrays.stream(SERVICE_TYPE_LABELS))
+ .toArray(String[]::new),
+ originalPtr.getReceiptTime(), originalPtr.getCacheFlush(), originalPtr.getTtl(),
+ originalPtr.getPointer()));
+ processResponse(new MdnsPacket(
+ packetWithoutSubtype.flags,
+ packetWithoutSubtype.questions,
+ newAnswers,
+ packetWithoutSubtype.authorityRecords,
+ packetWithoutSubtype.additionalRecords), socketKey);
+
+ final ArgumentMatcher<MdnsServiceInfo> subtypeInstanceMatcher = info ->
+ info.getServiceInstanceName().equals(matchingInstance)
+ && info.getSubtypes().equals(List.of("_subtype", "_othersub"));
+
+ // Service found callbacks are sent now
+ inOrder.verify(mockListenerOne).onServiceNameDiscovered(
+ argThat(subtypeInstanceMatcher), eq(false) /* isServiceFromCache */);
+ inOrder.verify(mockListenerOne).onServiceFound(
+ argThat(subtypeInstanceMatcher), eq(false) /* isServiceFromCache */);
+
+ // Address update: update callbacks are sent
+ processResponse(createResponse(
+ matchingInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap(), TEST_TTL), socketKey);
+
+ inOrder.verify(mockListenerOne).onServiceUpdated(argThat(info ->
+ subtypeInstanceMatcher.matches(info)
+ && info.getIpv4Addresses().equals(List.of(ipV4Address))
+ && info.getIpv6Addresses().equals(List.of(ipV6Address))));
+
+ // Goodbye: service removed callbacks are sent
+ processResponse(createResponse(
+ matchingInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap(), 0L /* ttl */), socketKey);
+
+ inOrder.verify(mockListenerOne).onServiceRemoved(matchServiceName(matchingInstance));
+ inOrder.verify(mockListenerOne).onServiceNameRemoved(matchServiceName(matchingInstance));
+ }
+
+ @Test
+ public void testNotifySocketDestroyed() throws Exception {
final String requestedInstance = "instance1";
final String otherInstance = "instance2";
final String ipV4Address = "192.0.2.0";
@@ -1666,6 +1825,243 @@
socketKey);
}
+ @Test
+ public void sendQueries_aggressiveScanMode() {
+ final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(AGGRESSIVE_QUERY_MODE).build();
+ startSendAndReceive(mockListenerOne, searchOptions);
+ // Always try to remove the task.
+ verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+ int burstCounter = 0;
+ int betweenBurstTime = 0;
+ for (int i = 0; i < expectedIPv4Packets.length; i += 3) {
+ verifyAndSendQuery(i, betweenBurstTime, /* expectsUnicastResponse= */ true);
+ verifyAndSendQuery(i + 1, /* timeInMs= */ 0, /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(i + 2, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+ /* expectsUnicastResponse= */ false);
+ betweenBurstTime = Math.min(
+ INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS * (int) Math.pow(2, burstCounter),
+ MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS);
+ burstCounter++;
+ }
+ // Verify that Task is not removed before stopSendAndReceive was called.
+ verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+ // Stop sending packets.
+ stopSendAndReceive(mockListenerOne);
+ verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+ }
+
+ @Test
+ public void sendQueries_reentry_aggressiveScanMode() {
+ final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE).setQueryMode(AGGRESSIVE_QUERY_MODE).build();
+ startSendAndReceive(mockListenerOne, searchOptions);
+ // Always try to remove the task.
+ verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+ // First burst, first query is sent.
+ verifyAndSendQuery(0, /* timeInMs= */ 0, /* expectsUnicastResponse= */ true);
+
+ // After the first query is sent, change the subtypes, and restart.
+ final MdnsSearchOptions searchOptions2 = MdnsSearchOptions.newBuilder().addSubtype(SUBTYPE)
+ .addSubtype("_subtype2").setQueryMode(AGGRESSIVE_QUERY_MODE).build();
+ startSendAndReceive(mockListenerOne, searchOptions2);
+ // The previous scheduled task should be canceled.
+ verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+ // Queries should continue to be sent.
+ verifyAndSendQuery(1, /* timeInMs= */ 0, /* expectsUnicastResponse= */ true);
+ verifyAndSendQuery(2, /* timeInMs= */ 0, /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(3, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+ /* expectsUnicastResponse= */ false);
+
+ // Stop sending packets.
+ stopSendAndReceive(mockListenerOne);
+ verify(mockDeps, times(3)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+ }
+
+ @Test
+ public void sendQueries_blendScanWithQueryBackoff() {
+ final int numOfQueriesBeforeBackoff = 11;
+ final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .addSubtype(SUBTYPE)
+ .setQueryMode(AGGRESSIVE_QUERY_MODE)
+ .setNumOfQueriesBeforeBackoff(numOfQueriesBeforeBackoff)
+ .build();
+ startSendAndReceive(mockListenerOne, searchOptions);
+ // Always try to remove the task.
+ verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+ int burstCounter = 0;
+ int betweenBurstTime = 0;
+ for (int i = 0; i < numOfQueriesBeforeBackoff; i += 3) {
+ verifyAndSendQuery(i, betweenBurstTime, /* expectsUnicastResponse= */ true);
+ verifyAndSendQuery(i + 1, /* timeInMs= */ 0, /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(i + 2, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+ /* expectsUnicastResponse= */ false);
+ betweenBurstTime = Math.min(
+ INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS * (int) Math.pow(2, burstCounter),
+ MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS);
+ burstCounter++;
+ }
+ // In backoff mode, the current scheduled task will be canceled and reschedule if the
+ // 0.8 * smallestRemainingTtl is larger than time to next run.
+ long currentTime = TEST_TTL / 2 + TEST_ELAPSED_REALTIME;
+ doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+ doReturn(true).when(mockDeps).hasMessages(any(), eq(EVENT_START_QUERYTASK));
+ processResponse(createResponse(
+ "service-instance-1", "192.0.2.123", 5353,
+ SERVICE_TYPE_LABELS,
+ Collections.emptyMap(), TEST_TTL), socketKey);
+ verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+ assertNotNull(delayMessage);
+ verifyAndSendQuery(12 /* index */, (long) (TEST_TTL / 2 * 0.8) /* timeInMs */,
+ true /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+ 14 /* scheduledCount */);
+ currentTime += (long) (TEST_TTL / 2 * 0.8);
+ doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+ verifyAndSendQuery(13 /* index */, 0 /* timeInMs */,
+ false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+ 15 /* scheduledCount */);
+ verifyAndSendQuery(14 /* index */, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+ false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
+ 16 /* scheduledCount */);
+ }
+
+ @Test
+ public void testSendQueryWithKnownAnswers() throws Exception {
+ client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
+ mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
+ serviceCache,
+ MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
+
+ doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
+ any(), any(MdnsPacket.class), any(InetSocketAddress.class), anyBoolean());
+
+ startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
+
+ final ArgumentCaptor<List<DatagramPacket>> queryCaptor =
+ ArgumentCaptor.forClass(List.class);
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+ // Send twice for IPv4 and IPv6
+ inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
+ queryCaptor.capture(), eq(socketKey), eq(false));
+ verify(mockDeps, times(1)).sendMessage(any(), any(Message.class));
+ assertNotNull(delayMessage);
+
+ final MdnsPacket queryPacket = MdnsPacket.parse(
+ new MdnsPacketReader(queryCaptor.getValue().get(0)));
+ assertTrue(hasQuestion(queryPacket, MdnsRecord.TYPE_PTR));
+
+ // Process a response
+ final String serviceName = "service-instance";
+ final String ipV4Address = "192.0.2.0";
+ final String[] subtypeLabels = Stream.concat(Stream.of("_subtype", "_sub"),
+ Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
+ final MdnsPacket packetWithoutSubtype = createResponse(
+ serviceName, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap() /* textAttributes */, TEST_TTL);
+ final MdnsPointerRecord originalPtr = (MdnsPointerRecord) CollectionUtils.findFirst(
+ packetWithoutSubtype.answers, r -> r instanceof MdnsPointerRecord);
+
+ // Add a subtype PTR record
+ final ArrayList<MdnsRecord> newAnswers = new ArrayList<>(packetWithoutSubtype.answers);
+ newAnswers.add(new MdnsPointerRecord(subtypeLabels, originalPtr.getReceiptTime(),
+ originalPtr.getCacheFlush(), originalPtr.getTtl(), originalPtr.getPointer()));
+ final MdnsPacket packetWithSubtype = new MdnsPacket(
+ packetWithoutSubtype.flags,
+ packetWithoutSubtype.questions,
+ newAnswers,
+ packetWithoutSubtype.authorityRecords,
+ packetWithoutSubtype.additionalRecords);
+ processResponse(packetWithSubtype, socketKey);
+
+ // Expect a query with known answers
+ dispatchMessage();
+ final ArgumentCaptor<List<DatagramPacket>> knownAnswersQueryCaptor =
+ ArgumentCaptor.forClass(List.class);
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+ inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
+ knownAnswersQueryCaptor.capture(), eq(socketKey), eq(false));
+
+ final MdnsPacket knownAnswersQueryPacket = MdnsPacket.parse(
+ new MdnsPacketReader(knownAnswersQueryCaptor.getValue().get(0)));
+ assertTrue(hasQuestion(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertTrue(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertFalse(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
+ }
+
+ @Test
+ public void testSendQueryWithSubTypeWithKnownAnswers() throws Exception {
+ client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
+ mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
+ serviceCache,
+ MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
+
+ doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
+ any(), any(MdnsPacket.class), any(InetSocketAddress.class), anyBoolean());
+
+ final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
+ .addSubtype("subtype").build();
+ startSendAndReceive(mockListenerOne, options);
+ InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
+
+ final ArgumentCaptor<List<DatagramPacket>> queryCaptor =
+ ArgumentCaptor.forClass(List.class);
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+ // Send twice for IPv4 and IPv6
+ inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
+ queryCaptor.capture(), eq(socketKey), eq(false));
+ verify(mockDeps, times(1)).sendMessage(any(), any(Message.class));
+ assertNotNull(delayMessage);
+
+ final MdnsPacket queryPacket = MdnsPacket.parse(
+ new MdnsPacketReader(queryCaptor.getValue().get(0)));
+ final String[] subtypeLabels = Stream.concat(Stream.of("_subtype", "_sub"),
+ Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
+ assertTrue(hasQuestion(queryPacket, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertTrue(hasQuestion(queryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
+
+ // Process a response
+ final String serviceName = "service-instance";
+ final String ipV4Address = "192.0.2.0";
+ final MdnsPacket packetWithoutSubtype = createResponse(
+ serviceName, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap() /* textAttributes */, TEST_TTL);
+ final MdnsPointerRecord originalPtr = (MdnsPointerRecord) CollectionUtils.findFirst(
+ packetWithoutSubtype.answers, r -> r instanceof MdnsPointerRecord);
+
+ // Add a subtype PTR record
+ final ArrayList<MdnsRecord> newAnswers = new ArrayList<>(packetWithoutSubtype.answers);
+ newAnswers.add(new MdnsPointerRecord(subtypeLabels, originalPtr.getReceiptTime(),
+ originalPtr.getCacheFlush(), originalPtr.getTtl(), originalPtr.getPointer()));
+ final MdnsPacket packetWithSubtype = new MdnsPacket(
+ packetWithoutSubtype.flags,
+ packetWithoutSubtype.questions,
+ newAnswers,
+ packetWithoutSubtype.authorityRecords,
+ packetWithoutSubtype.additionalRecords);
+ processResponse(packetWithSubtype, socketKey);
+
+ // Expect a query with known answers
+ dispatchMessage();
+ final ArgumentCaptor<List<DatagramPacket>> knownAnswersQueryCaptor =
+ ArgumentCaptor.forClass(List.class);
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+ inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
+ knownAnswersQueryCaptor.capture(), eq(socketKey), eq(false));
+
+ final MdnsPacket knownAnswersQueryPacket = MdnsPacket.parse(
+ new MdnsPacketReader(knownAnswersQueryCaptor.getValue().get(0)));
+ assertTrue(hasQuestion(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertTrue(hasQuestion(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
+ assertTrue(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, SERVICE_TYPE_LABELS));
+ assertTrue(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
+ }
+
private static MdnsServiceInfo matchServiceName(String name) {
return argThat(info -> info.getServiceInstanceName().equals(name));
}
@@ -1687,17 +2083,21 @@
currentThreadExecutor.getAndClearLastScheduledRunnable().run();
if (expectsUnicastResponse) {
verify(mockSocketClient).sendPacketRequestingUnicastResponse(
- expectedIPv4Packets[index], socketKey, false);
+ argThat(pkts -> pkts.get(0).equals(expectedIPv4Packets[index])),
+ eq(socketKey), eq(false));
if (multipleSocketDiscovery) {
verify(mockSocketClient).sendPacketRequestingUnicastResponse(
- expectedIPv6Packets[index], socketKey, false);
+ argThat(pkts -> pkts.get(0).equals(expectedIPv6Packets[index])),
+ eq(socketKey), eq(false));
}
} else {
verify(mockSocketClient).sendPacketRequestingMulticastResponse(
- expectedIPv4Packets[index], socketKey, false);
+ argThat(pkts -> pkts.get(0).equals(expectedIPv4Packets[index])),
+ eq(socketKey), eq(false));
if (multipleSocketDiscovery) {
verify(mockSocketClient).sendPacketRequestingMulticastResponse(
- expectedIPv6Packets[index], socketKey, false);
+ argThat(pkts -> pkts.get(0).equals(expectedIPv6Packets[index])),
+ eq(socketKey), eq(false));
}
}
verify(mockDeps, times(index + 1))
@@ -1712,6 +2112,11 @@
Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
}
+ private static String[] getServiceTypeWithSubtype(String subtype) {
+ return Stream.concat(Stream.of(subtype, "_sub"),
+ Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
+ }
+
private static boolean hasQuestion(MdnsPacket packet, int type) {
return hasQuestion(packet, type, null);
}
@@ -1721,6 +2126,12 @@
&& (name == null || Arrays.equals(q.name, name)));
}
+ private static boolean hasAnswer(MdnsPacket packet, int type, @NonNull String[] name) {
+ return packet.answers.stream().anyMatch(q -> {
+ return q.getType() == type && (Arrays.equals(q.name, name));
+ });
+ }
+
// A fake ScheduledExecutorService that keeps tracking the last scheduled Runnable and its delay
// time.
private class FakeExecutor extends ScheduledThreadPoolExecutor {
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
index 8b7ab71..1989ed3 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
@@ -26,14 +26,18 @@
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.Manifest.permission;
import android.annotation.RequiresPermission;
import android.content.Context;
+import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.MulticastLock;
import android.text.format.DateUtils;
@@ -48,7 +52,9 @@
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
@@ -56,6 +62,8 @@
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -71,6 +79,7 @@
@Mock private Context mContext;
@Mock private WifiManager mockWifiManager;
+ @Mock private ConnectivityManager mockConnectivityManager;
@Mock private MdnsSocket mockMulticastSocket;
@Mock private MdnsSocket mockUnicastSocket;
@Mock private MulticastLock mockMulticastLock;
@@ -84,6 +93,9 @@
public void setup() throws RuntimeException, IOException {
MockitoAnnotations.initMocks(this);
+ doReturn(mockConnectivityManager).when(mContext).getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+
when(mockWifiManager.createMulticastLock(ArgumentMatchers.anyString()))
.thenReturn(mockMulticastLock);
@@ -226,7 +238,7 @@
// Sends a packet.
DatagramPacket packet = getTestDatagramPacket();
- mdnsClient.sendPacketRequestingMulticastResponse(packet,
+ mdnsClient.sendPacketRequestingMulticastResponse(List.of(packet),
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
// mockMulticastSocket.send() will be called on another thread. If we verify it immediately,
// it may not be called yet. So timeout is added.
@@ -234,7 +246,7 @@
verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
// Verify the packet is sent by the unicast socket.
- mdnsClient.sendPacketRequestingUnicastResponse(packet,
+ mdnsClient.sendPacketRequestingUnicastResponse(List.of(packet),
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
verify(mockUnicastSocket, timeout(TIMEOUT).times(1)).send(packet);
@@ -279,7 +291,7 @@
// Sends a packet.
DatagramPacket packet = getTestDatagramPacket();
- mdnsClient.sendPacketRequestingMulticastResponse(packet,
+ mdnsClient.sendPacketRequestingMulticastResponse(List.of(packet),
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
// mockMulticastSocket.send() will be called on another thread. If we verify it immediately,
// it may not be called yet. So timeout is added.
@@ -287,7 +299,7 @@
verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
// Verify the packet is sent by the multicast socket as well.
- mdnsClient.sendPacketRequestingUnicastResponse(packet,
+ mdnsClient.sendPacketRequestingUnicastResponse(List.of(packet),
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
verify(mockMulticastSocket, timeout(TIMEOUT).times(2)).send(packet);
verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
@@ -320,19 +332,25 @@
@Test
public void testStartStop() throws IOException {
- for (int i = 0; i < 5; i++) {
+ for (int i = 1; i <= 5; i++) {
mdnsClient.startDiscovery();
Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
Thread socketThread = mdnsClient.sendThread;
+ final ArgumentCaptor<ConnectivityManager.NetworkCallback> cbCaptor =
+ ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
assertTrue(multicastReceiverThread.isAlive());
assertTrue(socketThread.isAlive());
+ verify(mockConnectivityManager, times(i))
+ .registerNetworkCallback(any(), cbCaptor.capture());
mdnsClient.stopDiscovery();
assertFalse(multicastReceiverThread.isAlive());
assertFalse(socketThread.isAlive());
+ verify(mockConnectivityManager, times(i))
+ .unregisterNetworkCallback(cbCaptor.getValue());
}
}
@@ -340,7 +358,7 @@
public void testStopDiscovery_queueIsCleared() throws IOException {
mdnsClient.startDiscovery();
mdnsClient.stopDiscovery();
- mdnsClient.sendPacketRequestingMulticastResponse(getTestDatagramPacket(),
+ mdnsClient.sendPacketRequestingMulticastResponse(List.of(getTestDatagramPacket()),
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
synchronized (mdnsClient.multicastPacketQueue) {
@@ -352,7 +370,7 @@
public void testSendPacket_afterDiscoveryStops() throws IOException {
mdnsClient.startDiscovery();
mdnsClient.stopDiscovery();
- mdnsClient.sendPacketRequestingMulticastResponse(getTestDatagramPacket(),
+ mdnsClient.sendPacketRequestingMulticastResponse(List.of(getTestDatagramPacket()),
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
synchronized (mdnsClient.multicastPacketQueue) {
@@ -366,7 +384,7 @@
//MdnsConfigsFlagsImpl.mdnsPacketQueueMaxSize.override(2L);
mdnsClient.startDiscovery();
for (int i = 0; i < 100; i++) {
- mdnsClient.sendPacketRequestingMulticastResponse(getTestDatagramPacket(),
+ mdnsClient.sendPacketRequestingMulticastResponse(List.of(getTestDatagramPacket()),
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
}
@@ -464,9 +482,9 @@
mdnsClient.startDiscovery();
DatagramPacket packet = getTestDatagramPacket();
- mdnsClient.sendPacketRequestingUnicastResponse(packet,
+ mdnsClient.sendPacketRequestingUnicastResponse(List.of(packet),
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
- mdnsClient.sendPacketRequestingMulticastResponse(packet,
+ mdnsClient.sendPacketRequestingMulticastResponse(List.of(packet),
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
// Wait for the timer to be triggered.
@@ -497,9 +515,9 @@
assertFalse(mdnsClient.receivedUnicastResponse);
assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
- mdnsClient.sendPacketRequestingUnicastResponse(packet,
+ mdnsClient.sendPacketRequestingUnicastResponse(List.of(packet),
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
- mdnsClient.sendPacketRequestingMulticastResponse(packet,
+ mdnsClient.sendPacketRequestingMulticastResponse(List.of(packet),
false /* onlyUseIpv6OnIpv6OnlyNetworks */);
Thread.sleep(MdnsConfigs.checkMulticastResponseIntervalMs() * 2);
@@ -556,6 +574,26 @@
.onResponseReceived(any(), argThat(key -> key.getInterfaceIndex() == -1));
}
+ @Test
+ public void testSendPacketWithMultipleDatagramPacket() throws IOException {
+ mdnsClient.startDiscovery();
+ final List<DatagramPacket> packets = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ packets.add(new DatagramPacket(new byte[10 + i] /* buff */, 0 /* offset */,
+ 10 + i /* length */, MdnsConstants.IPV4_SOCKET_ADDR));
+ }
+
+ // Sends packets.
+ mdnsClient.sendPacketRequestingMulticastResponse(packets,
+ false /* onlyUseIpv6OnIpv6OnlyNetworks */);
+ InOrder inOrder = inOrder(mockMulticastSocket);
+ for (int i = 0; i < 10; i++) {
+ // mockMulticastSocket.send() will be called on another thread. If we verify it
+ // immediately, it may not be called yet. So timeout is added.
+ inOrder.verify(mockMulticastSocket, timeout(TIMEOUT)).send(packets.get(i));
+ }
+ }
+
private DatagramPacket getTestDatagramPacket() {
return new DatagramPacket(buf, 0, 5,
new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), 5353 /* port */));
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
index f705bcb..009205e 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
@@ -17,6 +17,13 @@
package com.android.server.connectivity.mdns.util
import android.os.Build
+import com.android.server.connectivity.mdns.MdnsConstants
+import com.android.server.connectivity.mdns.MdnsConstants.FLAG_TRUNCATED
+import com.android.server.connectivity.mdns.MdnsPacket
+import com.android.server.connectivity.mdns.MdnsPacketReader
+import com.android.server.connectivity.mdns.MdnsPointerRecord
+import com.android.server.connectivity.mdns.MdnsRecord
+import com.android.server.connectivity.mdns.util.MdnsUtils.createQueryDatagramPackets
import com.android.server.connectivity.mdns.util.MdnsUtils.equalsDnsLabelIgnoreDnsCase
import com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase
import com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLabelsLowerCase
@@ -24,6 +31,8 @@
import com.android.server.connectivity.mdns.util.MdnsUtils.truncateServiceName
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
+import java.net.DatagramPacket
+import kotlin.test.assertContentEquals
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -43,19 +52,27 @@
assertEquals("ţést", toDnsLowerCase("ţést"))
// Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
// Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
- assertEquals("test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
- toDnsLowerCase("Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- "))
+ assertEquals(
+ "test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
+ toDnsLowerCase("Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ")
+ )
// Also test some characters where the first surrogate is not \ud800
- assertEquals("test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+ assertEquals(
+ "test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
"\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
- toDnsLowerCase("Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
- "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"))
+ toDnsLowerCase(
+ "Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+ "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
+ )
+ )
}
@Test
fun testToDnsLabelsLowerCase() {
- assertArrayEquals(arrayOf("test", "tÉst", "ţést"),
- toDnsLabelsLowerCase(arrayOf("TeSt", "TÉST", "ţést")))
+ assertArrayEquals(
+ arrayOf("test", "tÉst", "ţést"),
+ toDnsLabelsLowerCase(arrayOf("TeSt", "TÉST", "ţést"))
+ )
}
@Test
@@ -67,13 +84,17 @@
assertFalse(equalsIgnoreDnsCase("ŢÉST", "ţést"))
// Unicode characters 0x10000 (𐀀), 0x10001 (𐀁), 0x10041 (𐁁)
// Note the last 2 bytes of 0x10041 are identical to 'A', but it should remain unchanged.
- assertTrue(equalsIgnoreDnsCase("test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
- "Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- "))
+ assertTrue(equalsIgnoreDnsCase(
+ "test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- ",
+ "Test: -->\ud800\udc00 \ud800\udc01 \ud800\udc41<-- "
+ ))
// Also test some characters where the first surrogate is not \ud800
- assertTrue(equalsIgnoreDnsCase("test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
+ assertTrue(equalsIgnoreDnsCase(
+ "test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
"\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<",
"Test: >\ud83c\udff4\udb40\udc67\udb40\udc62\udb40" +
- "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"))
+ "\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f<"
+ ))
}
@Test
@@ -92,14 +113,84 @@
@Test
fun testTypeEqualsOrIsSubtype() {
- assertTrue(MdnsUtils.typeEqualsOrIsSubtype(arrayOf("_type", "_tcp", "local"),
- arrayOf("_type", "_TCP", "local")))
- assertTrue(MdnsUtils.typeEqualsOrIsSubtype(arrayOf("_type", "_tcp", "local"),
- arrayOf("a", "_SUB", "_type", "_TCP", "local")))
- assertFalse(MdnsUtils.typeEqualsOrIsSubtype(arrayOf("_sub", "_type", "_tcp", "local"),
- arrayOf("_type", "_TCP", "local")))
+ assertTrue(MdnsUtils.typeEqualsOrIsSubtype(
+ arrayOf("_type", "_tcp", "local"),
+ arrayOf("_type", "_TCP", "local")
+ ))
+ assertTrue(MdnsUtils.typeEqualsOrIsSubtype(
+ arrayOf("_type", "_tcp", "local"),
+ arrayOf("a", "_SUB", "_type", "_TCP", "local")
+ ))
+ assertFalse(MdnsUtils.typeEqualsOrIsSubtype(
+ arrayOf("_sub", "_type", "_tcp", "local"),
+ arrayOf("_type", "_TCP", "local")
+ ))
assertFalse(MdnsUtils.typeEqualsOrIsSubtype(
arrayOf("a", "_other", "_type", "_tcp", "local"),
- arrayOf("a", "_SUB", "_type", "_TCP", "local")))
+ arrayOf("a", "_SUB", "_type", "_TCP", "local")
+ ))
+ }
+
+ @Test
+ fun testCreateQueryDatagramPackets() {
+ // Question data bytes:
+ // Name label(17)(duplicated labels) + PTR type(2) + cacheFlush(2) = 21
+ //
+ // Known answers data bytes:
+ // Name label(17)(duplicated labels) + PTR type(2) + cacheFlush(2) + receiptTimeMillis(4)
+ // + Data length(2) + Pointer data(18)(duplicated labels) = 45
+ val questions = mutableListOf<MdnsRecord>()
+ val knownAnswers = mutableListOf<MdnsRecord>()
+ for (i in 1..100) {
+ questions.add(MdnsPointerRecord(arrayOf("_testservice$i", "_tcp", "local"), false))
+ knownAnswers.add(MdnsPointerRecord(
+ arrayOf("_testservice$i", "_tcp", "local"),
+ 0L,
+ false,
+ 4_500_000L,
+ arrayOf("MyTestService$i", "_testservice$i", "_tcp", "local")
+ ))
+ }
+ // MdnsPacket data bytes:
+ // Questions(21 * 100) + Answers(45 * 100) = 6600 -> at least 5 packets
+ val query = MdnsPacket(
+ MdnsConstants.FLAGS_QUERY,
+ questions as List<MdnsRecord>,
+ knownAnswers as List<MdnsRecord>,
+ emptyList(),
+ emptyList()
+ )
+ // Expect the oversize MdnsPacket to be separated into 5 DatagramPackets.
+ val bufferSize = 1500
+ val packets = createQueryDatagramPackets(
+ ByteArray(bufferSize),
+ query,
+ MdnsConstants.IPV4_SOCKET_ADDR
+ )
+ assertEquals(5, packets.size)
+ assertTrue(packets.all { packet -> packet.length < bufferSize })
+
+ val mdnsPacket = createMdnsPacketFromMultipleDatagramPackets(packets)
+ assertEquals(query.flags, mdnsPacket.flags)
+ assertContentEquals(query.questions, mdnsPacket.questions)
+ assertContentEquals(query.answers, mdnsPacket.answers)
+ }
+
+ private fun createMdnsPacketFromMultipleDatagramPackets(
+ packets: List<DatagramPacket>
+ ): MdnsPacket {
+ var flags = 0
+ val questions = mutableListOf<MdnsRecord>()
+ val answers = mutableListOf<MdnsRecord>()
+ for ((index, packet) in packets.withIndex()) {
+ val mdnsPacket = MdnsPacket.parse(MdnsPacketReader(packet))
+ if (index != packets.size - 1) {
+ assertTrue((mdnsPacket.flags and FLAG_TRUNCATED) == FLAG_TRUNCATED)
+ }
+ flags = mdnsPacket.flags
+ questions.addAll(mdnsPacket.questions)
+ answers.addAll(mdnsPacket.answers)
+ }
+ return MdnsPacket(flags, questions, answers, emptyList(), emptyList())
}
}
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/CSCaptivePortalAppTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
new file mode 100644
index 0000000..0bad60da
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.Manifest.permission.NETWORK_STACK
+import android.content.Intent
+import android.content.pm.PackageManager.PERMISSION_DENIED
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.net.CaptivePortal
+import android.net.ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN
+import android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.NetworkStack
+import android.net.RouteInfo
+import android.os.Build
+import android.os.Bundle
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkCallback
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+// This allows keeping all the networks connected without having to file individual requests
+// for them.
+private fun keepScore() = FromS(
+ NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build()
+)
+
+private fun nc(transport: Int, vararg caps: Int) = NetworkCapabilities.Builder().apply {
+ addTransportType(transport)
+ caps.forEach {
+ addCapability(it)
+ }
+ // Useful capabilities for everybody
+ addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ addCapability(NET_CAPABILITY_NOT_ROAMING)
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+}.build()
+
+private fun lp(iface: String) = LinkProperties().apply {
+ interfaceName = iface
+ addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+ addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+}
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.R)
+class CSCaptivePortalAppTest : CSTest() {
+ private val WIFI_IFACE = "wifi0"
+ private val TEST_REDIRECT_URL = "http://example.com/firstPath"
+ private val TIMEOUT_MS = 2_000L
+
+ @Test
+ fun testCaptivePortalApp_Reevaluate_Nopermission() {
+ val captivePortalCallback = TestableNetworkCallback()
+ val captivePortalRequest = NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build()
+ cm.registerNetworkCallback(captivePortalRequest, captivePortalCallback)
+ val wifiAgent = createWifiAgent()
+ wifiAgent.connectWithCaptivePortal(TEST_REDIRECT_URL)
+ captivePortalCallback.expectAvailableCallbacksUnvalidated(wifiAgent)
+ val signInIntent = startCaptivePortalApp(wifiAgent)
+ // Remove the granted permissions
+ context.setPermission(
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ PERMISSION_DENIED
+ )
+ context.setPermission(NETWORK_STACK, PERMISSION_DENIED)
+ val captivePortal: CaptivePortal? = signInIntent.getParcelableExtra(EXTRA_CAPTIVE_PORTAL)
+ captivePortal?.reevaluateNetwork()
+ verify(wifiAgent.networkMonitor, never()).forceReevaluation(anyInt())
+ }
+
+ private fun createWifiAgent(): CSAgentWrapper {
+ return Agent(
+ score = keepScore(),
+ lp = lp(WIFI_IFACE),
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET)
+ )
+ }
+
+ private fun startCaptivePortalApp(networkAgent: CSAgentWrapper): Intent {
+ val network = networkAgent.network
+ cm.startCaptivePortalApp(network)
+ waitForIdle()
+ verify(networkAgent.networkMonitor).launchCaptivePortalApp()
+
+ val testBundle = Bundle()
+ val testKey = "testkey"
+ val testValue = "testvalue"
+ testBundle.putString(testKey, testValue)
+ context.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_GRANTED)
+ cm.startCaptivePortalApp(network, testBundle)
+ val signInIntent: Intent = context.expectStartActivityIntent(TIMEOUT_MS)
+ assertEquals(ACTION_CAPTIVE_PORTAL_SIGN_IN, signInIntent.getAction())
+ assertEquals(testValue, signInIntent.getStringExtra(testKey))
+ return signInIntent
+ }
+}
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/CSFirewallChainTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSFirewallChainTest.kt
new file mode 100644
index 0000000..16de4da
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSFirewallChainTest.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.ConnectivityManager
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.server.connectivity.ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.any
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class CSFirewallChainTest : CSTest() {
+ @get:Rule
+ val ignoreRule = DevSdkIgnoreRule()
+
+ // Tests for setFirewallChainEnabled on FIREWALL_CHAIN_BACKGROUND
+ @Test
+ @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, false)])
+ fun setFirewallChainEnabled_backgroundChainDisabled() {
+ verifySetFirewallChainEnabledOnBackgroundDoesNothing()
+ }
+
+ @Test
+ @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, true)])
+ @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun setFirewallChainEnabled_backgroundChainEnabled_afterU() {
+ cm.setFirewallChainEnabled(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, true)
+ verify(bpfNetMaps).setChildChain(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, true)
+
+ clearInvocations(bpfNetMaps)
+
+ cm.setFirewallChainEnabled(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, false)
+ verify(bpfNetMaps).setChildChain(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, false)
+ }
+
+ @Test
+ @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, true)])
+ @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun setFirewallChainEnabled_backgroundChainEnabled_uptoU() {
+ verifySetFirewallChainEnabledOnBackgroundDoesNothing()
+ }
+
+ private fun verifySetFirewallChainEnabledOnBackgroundDoesNothing() {
+ cm.setFirewallChainEnabled(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, true)
+ verify(bpfNetMaps, never()).setChildChain(anyInt(), anyBoolean())
+
+ cm.setFirewallChainEnabled(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, false)
+ verify(bpfNetMaps, never()).setChildChain(anyInt(), anyBoolean())
+ }
+
+ // Tests for replaceFirewallChain on FIREWALL_CHAIN_BACKGROUND
+ @Test
+ @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, false)])
+ fun replaceFirewallChain_backgroundChainDisabled() {
+ verifyReplaceFirewallChainOnBackgroundDoesNothing()
+ }
+
+ @Test
+ @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, true)])
+ @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun replaceFirewallChain_backgroundChainEnabled_afterU() {
+ val uids = intArrayOf(53, 42, 79)
+ cm.replaceFirewallChain(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uids)
+ verify(bpfNetMaps).replaceUidChain(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uids)
+ }
+
+ @Test
+ @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, true)])
+ @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun replaceFirewallChain_backgroundChainEnabled_uptoU() {
+ verifyReplaceFirewallChainOnBackgroundDoesNothing()
+ }
+
+ private fun verifyReplaceFirewallChainOnBackgroundDoesNothing() {
+ val uids = intArrayOf(53, 42, 79)
+ cm.replaceFirewallChain(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uids)
+ verify(bpfNetMaps, never()).replaceUidChain(anyInt(), any(IntArray::class.java))
+ }
+
+ // Tests for setUidFirewallRule on FIREWALL_CHAIN_BACKGROUND
+ @Test
+ @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, false)])
+ fun setUidFirewallRule_backgroundChainDisabled() {
+ verifySetUidFirewallRuleOnBackgroundDoesNothing()
+ }
+
+ @Test
+ @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, true)])
+ @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun setUidFirewallRule_backgroundChainEnabled_afterU() {
+ val uid = 2345
+
+ cm.setUidFirewallRule(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uid,
+ ConnectivityManager.FIREWALL_RULE_DEFAULT)
+ verify(bpfNetMaps).setUidRule(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uid,
+ ConnectivityManager.FIREWALL_RULE_DENY)
+
+ clearInvocations(bpfNetMaps)
+
+ cm.setUidFirewallRule(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uid,
+ ConnectivityManager.FIREWALL_RULE_DENY)
+ verify(bpfNetMaps).setUidRule(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uid,
+ ConnectivityManager.FIREWALL_RULE_DENY)
+
+ clearInvocations(bpfNetMaps)
+
+ cm.setUidFirewallRule(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uid,
+ ConnectivityManager.FIREWALL_RULE_ALLOW)
+ verify(bpfNetMaps).setUidRule(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uid,
+ ConnectivityManager.FIREWALL_RULE_ALLOW)
+ }
+
+ @Test
+ @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, true)])
+ @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun setUidFirewallRule_backgroundChainEnabled_uptoU() {
+ verifySetUidFirewallRuleOnBackgroundDoesNothing()
+ }
+
+ private fun verifySetUidFirewallRuleOnBackgroundDoesNothing() {
+ val uid = 2345
+
+ listOf(ConnectivityManager.FIREWALL_RULE_DEFAULT, ConnectivityManager.FIREWALL_RULE_ALLOW,
+ ConnectivityManager.FIREWALL_RULE_DENY).forEach { rule ->
+ cm.setUidFirewallRule(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uid, rule)
+ verify(bpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt())
+ }
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
new file mode 100644
index 0000000..bb7fb51
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.InetAddresses
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.VpnManager.TYPE_VPN_SERVICE
+import android.net.VpnTransportInfo
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.server.connectivity.ConnectivityFlags
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+
+private const val VPN_IFNAME = "tun10041"
+private const val VPN_IFNAME2 = "tun10042"
+private const val WIFI_IFNAME = "wlan0"
+private const val TIMEOUT_MS = 1_000L
+private const val LONG_TIMEOUT_MS = 5_000
+
+private fun vpnNc() = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_VPN)
+ .removeCapability(NET_CAPABILITY_NOT_VPN)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .setTransportInfo(
+ VpnTransportInfo(
+ TYPE_VPN_SERVICE,
+ "MySession12345",
+ false /* bypassable */,
+ false /* longLivedTcpConnectionsExpensive */
+ )
+ )
+ .build()
+
+private fun wifiNc() = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+
+private fun nr(transport: Int) = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(transport).apply {
+ if (transport != TRANSPORT_VPN) {
+ addCapability(NET_CAPABILITY_NOT_VPN)
+ }
+ }.build()
+
+private fun lp(iface: String, vararg linkAddresses: LinkAddress) = LinkProperties().apply {
+ interfaceName = iface
+ for (linkAddress in linkAddresses) {
+ addLinkAddress(linkAddress)
+ }
+}
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class CSIngressDiscardRuleTests : CSTest() {
+ private val IPV6_ADDRESS = InetAddresses.parseNumericAddress("2001:db8:1::1")
+ private val IPV6_LINK_ADDRESS = LinkAddress(IPV6_ADDRESS, 64)
+ private val IPV6_ADDRESS2 = InetAddresses.parseNumericAddress("2001:db8:1::2")
+ private val IPV6_LINK_ADDRESS2 = LinkAddress(IPV6_ADDRESS2, 64)
+ private val IPV6_ADDRESS3 = InetAddresses.parseNumericAddress("2001:db8:1::3")
+ private val IPV6_LINK_ADDRESS3 = LinkAddress(IPV6_ADDRESS3, 64)
+ private val LOCAL_IPV6_ADDRRESS = InetAddresses.parseNumericAddress("fe80::1234")
+ private val LOCAL_IPV6_LINK_ADDRRESS = LinkAddress(LOCAL_IPV6_ADDRRESS, 64)
+
+ @Test
+ fun testVpnIngressDiscardRule_UpdateVpnAddress() {
+ // non-VPN network whose address will be not duplicated with VPN address
+ val wifiNc = wifiNc()
+ val wifiLp = lp(WIFI_IFNAME, IPV6_LINK_ADDRESS3)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+
+ val nr = nr(TRANSPORT_VPN)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(nr, cb)
+ val nc = vpnNc()
+ val lp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val agent = Agent(nc = nc, lp = lp)
+ agent.connect()
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+
+ // IngressDiscardRule is added to the VPN address
+ verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+ verify(bpfNetMaps, never()).setIngressDiscardRule(LOCAL_IPV6_ADDRRESS, VPN_IFNAME)
+
+ // The VPN address is changed
+ val newLp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS2, LOCAL_IPV6_LINK_ADDRRESS)
+ agent.sendLinkProperties(newLp)
+ cb.expect<LinkPropertiesChanged>(agent.network)
+
+ // IngressDiscardRule is removed from the old VPN address and added to the new VPN address
+ verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS)
+ verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS2, VPN_IFNAME)
+ verify(bpfNetMaps, never()).setIngressDiscardRule(LOCAL_IPV6_ADDRRESS, VPN_IFNAME)
+
+ agent.disconnect()
+ verify(bpfNetMaps, timeout(TIMEOUT_MS)).removeIngressDiscardRule(IPV6_ADDRESS2)
+
+ cm.unregisterNetworkCallback(cb)
+ }
+
+ @Test
+ fun testVpnIngressDiscardRule_UpdateInterfaceName() {
+ val inorder = inOrder(bpfNetMaps)
+
+ val nr = nr(TRANSPORT_VPN)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(nr, cb)
+ val nc = vpnNc()
+ val lp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val agent = Agent(nc = nc, lp = lp)
+ agent.connect()
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+
+ // IngressDiscardRule is added to the VPN address
+ inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+ inorder.verifyNoMoreInteractions()
+
+ // The VPN interface name is changed
+ val newlp = lp(VPN_IFNAME2, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ agent.sendLinkProperties(newlp)
+ cb.expect<LinkPropertiesChanged>(agent.network)
+
+ // IngressDiscardRule is updated with the new interface name
+ inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME2)
+ inorder.verifyNoMoreInteractions()
+
+ agent.disconnect()
+ inorder.verify(bpfNetMaps, timeout(TIMEOUT_MS)).removeIngressDiscardRule(IPV6_ADDRESS)
+
+ cm.unregisterNetworkCallback(cb)
+ }
+
+ @Test
+ fun testVpnIngressDiscardRule_DuplicatedIpAddress_UpdateVpnAddress() {
+ val inorder = inOrder(bpfNetMaps)
+
+ val wifiNc = wifiNc()
+ val wifiLp = lp(WIFI_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+
+ // IngressDiscardRule is not added to non-VPN interfaces
+ inorder.verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
+
+ val nr = nr(TRANSPORT_VPN)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+ val vpnNc = vpnNc()
+ val vpnLp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val vpnAgent = Agent(nc = vpnNc, lp = vpnLp)
+ vpnAgent.connect()
+ cb.expectAvailableCallbacks(vpnAgent.network, validated = false)
+
+ // IngressDiscardRule is not added since the VPN address is duplicated with the Wi-Fi
+ // address
+ inorder.verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
+
+ // The VPN address is changed to a different address from the Wi-Fi interface
+ val newVpnlp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS2, LOCAL_IPV6_LINK_ADDRRESS)
+ vpnAgent.sendLinkProperties(newVpnlp)
+
+ // IngressDiscardRule is added to the VPN address since the VPN address is not duplicated
+ // with the Wi-Fi address
+ cb.expect<LinkPropertiesChanged>(vpnAgent.network)
+ inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS2, VPN_IFNAME)
+
+ // The VPN address is changed back to the same address as the Wi-Fi interface
+ vpnAgent.sendLinkProperties(vpnLp)
+ cb.expect<LinkPropertiesChanged>(vpnAgent.network)
+
+ // IngressDiscardRule for IPV6_ADDRESS2 is removed but IngressDiscardRule for
+ // IPV6_LINK_ADDRESS is not added since Wi-Fi also uses IPV6_LINK_ADDRESS
+ inorder.verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS2)
+ inorder.verifyNoMoreInteractions()
+
+ vpnAgent.disconnect()
+ inorder.verifyNoMoreInteractions()
+
+ cm.unregisterNetworkCallback(cb)
+ }
+
+ @Test
+ fun testVpnIngressDiscardRule_DuplicatedIpAddress_UpdateNonVpnAddress() {
+ val inorder = inOrder(bpfNetMaps)
+
+ val vpnNc = vpnNc()
+ val vpnLp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val vpnAgent = Agent(nc = vpnNc, lp = vpnLp)
+ vpnAgent.connect()
+
+ // IngressDiscardRule is added to the VPN address
+ inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+ inorder.verifyNoMoreInteractions()
+
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+ val wifiNc = wifiNc()
+ val wifiLp = lp(WIFI_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+ // IngressDiscardRule is removed since the VPN address is duplicated with the Wi-Fi address
+ inorder.verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS)
+
+ // The Wi-Fi address is changed to a different address from the VPN interface
+ val newWifilp = lp(WIFI_IFNAME, IPV6_LINK_ADDRESS2, LOCAL_IPV6_LINK_ADDRRESS)
+ wifiAgent.sendLinkProperties(newWifilp)
+ cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+ // IngressDiscardRule is added to the VPN address since the VPN address is not duplicated
+ // with the Wi-Fi address
+ inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+ inorder.verifyNoMoreInteractions()
+
+ // The Wi-Fi address is changed back to the same address as the VPN interface
+ wifiAgent.sendLinkProperties(wifiLp)
+ cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+ // IngressDiscardRule is removed since the VPN address is duplicated with the Wi-Fi address
+ inorder.verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS)
+
+ // IngressDiscardRule is added to the VPN address since Wi-Fi is disconnected
+ wifiAgent.disconnect()
+ inorder.verify(bpfNetMaps, timeout(TIMEOUT_MS))
+ .setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+
+ vpnAgent.disconnect()
+ inorder.verify(bpfNetMaps, timeout(TIMEOUT_MS)).removeIngressDiscardRule(IPV6_ADDRESS)
+
+ cm.unregisterNetworkCallback(cb)
+ }
+
+ @Test
+ fun testVpnIngressDiscardRule_UnregisterAfterReplacement() {
+ val wifiNc = wifiNc()
+ val wifiLp = lp(WIFI_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+ waitForIdle()
+
+ val vpnNc = vpnNc()
+ val vpnLp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val vpnAgent = Agent(nc = vpnNc, lp = vpnLp)
+ vpnAgent.connect()
+
+ // IngressDiscardRule is added since the Wi-Fi network is destroyed
+ verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+
+ // IngressDiscardRule is removed since the VPN network is destroyed
+ vpnAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+ waitForIdle()
+ verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS)
+ }
+
+ @Test @FeatureFlags([Flag(ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING, false)])
+ fun testVpnIngressDiscardRule_FeatureDisabled() {
+ val nr = nr(TRANSPORT_VPN)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(nr, cb)
+ val nc = vpnNc()
+ val lp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val agent = Agent(nc = nc, lp = lp)
+ agent.connect()
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+
+ // IngressDiscardRule should not be added since feature is disabled
+ verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
+ }
+}
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..83fff87 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
@@ -36,20 +38,25 @@
import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
import android.net.NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK
import android.net.RouteInfo
+import android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_LOCAL_NETWORK
import android.os.Build
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
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.ArgumentMatchers.any
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.times
import org.mockito.Mockito.verify
-import kotlin.test.assertFailsWith
+import org.mockito.Mockito.verifyNoMoreInteractions
private const val TIMEOUT_MS = 200L
private const val MEDIUM_TIMEOUT_MS = 1_000L
@@ -79,9 +86,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 +203,271 @@
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,
+ score = FromS(NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+ .build())
+ )
+ 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 +487,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 +509,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 +530,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
@@ -531,4 +833,59 @@
listenCb.expect<Lost>()
}
+
+ fun doTestLocalNetworkRequest(
+ request: NetworkRequest,
+ enableMatchLocalNetwork: Boolean,
+ expectCallback: Boolean
+ ) {
+ deps.setBuildSdk(VERSION_V)
+ deps.setChangeIdEnabled(enableMatchLocalNetwork, ENABLE_MATCH_LOCAL_NETWORK)
+
+ val requestCb = TestableNetworkCallback()
+ val listenCb = TestableNetworkCallback()
+ cm.requestNetwork(request, requestCb)
+ cm.registerNetworkCallback(request, listenCb)
+
+ val localAgent = createLocalAgent("local0", FromS(LocalNetworkConfig.Builder().build()))
+ localAgent.connect()
+
+ if (expectCallback) {
+ requestCb.expectAvailableCallbacks(localAgent.network, validated = false)
+ listenCb.expectAvailableCallbacks(localAgent.network, validated = false)
+ } else {
+ waitForIdle()
+ requestCb.assertNoCallback(timeoutMs = 0)
+ listenCb.assertNoCallback(timeoutMs = 0)
+ }
+ localAgent.disconnect()
+ }
+
+ @Test
+ fun testLocalNetworkRequest() {
+ val request = NetworkRequest.Builder().build()
+ // If ENABLE_MATCH_LOCAL_NETWORK is false, request is not satisfied by local network
+ doTestLocalNetworkRequest(
+ request,
+ enableMatchLocalNetwork = false,
+ expectCallback = false)
+ // If ENABLE_MATCH_LOCAL_NETWORK is true, request is satisfied by local network
+ doTestLocalNetworkRequest(
+ request,
+ enableMatchLocalNetwork = true,
+ expectCallback = true)
+ }
+
+ @Test
+ fun testLocalNetworkRequest_withCapability() {
+ val request = NetworkRequest.Builder().addCapability(NET_CAPABILITY_LOCAL_NETWORK).build()
+ doTestLocalNetworkRequest(
+ request,
+ enableMatchLocalNetwork = false,
+ expectCallback = true)
+ doTestLocalNetworkRequest(
+ request,
+ enableMatchLocalNetwork = true,
+ expectCallback = true)
+ }
}
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/CSSatelliteNetworkFallbackTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt
new file mode 100644
index 0000000..9024641
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkFallbackTest.kt
@@ -0,0 +1,202 @@
+/*
+ * 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.IpPrefix
+import android.net.INetd
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NativeNetworkConfig
+import android.net.NativeNetworkType
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkScore
+import android.net.NetworkCapabilities.TRANSPORT_SATELLITE
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.RouteInfo
+import android.net.UidRange
+import android.net.UidRangeParcel
+import android.net.VpnManager
+import android.net.netd.aidl.NativeUidRangeConfig
+import android.os.Build
+import android.os.UserHandle
+import android.util.ArraySet
+import com.android.net.module.util.CollectionUtils
+import com.android.server.ConnectivityService.PREFERENCE_ORDER_SATELLITE_FALLBACK
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.visibleOnHandlerThread
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+private const val SECONDARY_USER = 10
+private val SECONDARY_USER_HANDLE = UserHandle(SECONDARY_USER)
+private const val TEST_PACKAGE_UID = 123
+private const val TEST_PACKAGE_UID2 = 321
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class CSSatelliteNetworkPreferredTest : CSTest() {
+ /**
+ * Test createMultiLayerNrisFromSatelliteNetworkPreferredUids returns correct
+ * NetworkRequestInfo.
+ */
+ @Test
+ fun testCreateMultiLayerNrisFromSatelliteNetworkPreferredUids() {
+ // Verify that empty uid set should not create any NRI for it.
+ val nrisNoUid = service.createMultiLayerNrisFromSatelliteNetworkFallbackUids(emptySet())
+ Assert.assertEquals(0, nrisNoUid.size.toLong())
+ val uid1 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
+ val uid2 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID2)
+ val uid3 = SECONDARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
+ assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(mutableSetOf(uid1))
+ assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(mutableSetOf(uid1, uid3))
+ assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(mutableSetOf(uid1, uid2))
+ }
+
+ /**
+ * Test that SATELLITE_NETWORK_PREFERENCE_UIDS changes will send correct net id and uid ranges
+ * to netd.
+ */
+ @Test
+ fun testSatelliteNetworkPreferredUidsChanged() {
+ val netdInOrder = inOrder(netd)
+
+ val satelliteAgent = createSatelliteAgent("satellite0")
+ satelliteAgent.connect()
+
+ val satelliteNetId = satelliteAgent.network.netId
+ netdInOrder.verify(netd).networkCreate(
+ nativeNetworkConfigPhysical(satelliteNetId, INetd.PERMISSION_NONE))
+
+ val uid1 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
+ val uid2 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID2)
+ val uid3 = SECONDARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)
+
+ // Initial satellite network preferred uids status.
+ setAndUpdateSatelliteNetworkPreferredUids(setOf())
+ netdInOrder.verify(netd, never()).networkAddUidRangesParcel(any())
+ netdInOrder.verify(netd, never()).networkRemoveUidRangesParcel(any())
+
+ // Set SATELLITE_NETWORK_PREFERENCE_UIDS setting and verify that net id and uid ranges
+ // send to netd
+ var uids = mutableSetOf(uid1, uid2, uid3)
+ val uidRanges1 = toUidRangeStableParcels(uidRangesForUids(uids))
+ val config1 = NativeUidRangeConfig(
+ satelliteNetId, uidRanges1,
+ PREFERENCE_ORDER_SATELLITE_FALLBACK
+ )
+ setAndUpdateSatelliteNetworkPreferredUids(uids)
+ netdInOrder.verify(netd).networkAddUidRangesParcel(config1)
+ netdInOrder.verify(netd, never()).networkRemoveUidRangesParcel(any())
+
+ // Set SATELLITE_NETWORK_PREFERENCE_UIDS setting again and verify that old rules are removed
+ // and new rules are added.
+ uids = mutableSetOf(uid1)
+ val uidRanges2: Array<UidRangeParcel?> = toUidRangeStableParcels(uidRangesForUids(uids))
+ val config2 = NativeUidRangeConfig(
+ satelliteNetId, uidRanges2,
+ PREFERENCE_ORDER_SATELLITE_FALLBACK
+ )
+ setAndUpdateSatelliteNetworkPreferredUids(uids)
+ netdInOrder.verify(netd).networkRemoveUidRangesParcel(config1)
+ netdInOrder.verify(netd).networkAddUidRangesParcel(config2)
+ }
+
+ private fun assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(uids: Set<Int>) {
+ val nris: Set<ConnectivityService.NetworkRequestInfo> =
+ service.createMultiLayerNrisFromSatelliteNetworkFallbackUids(uids)
+ val nri = nris.iterator().next()
+ // Verify that one NRI is created with multilayer requests. Because one NRI can contain
+ // multiple uid ranges, so it only need create one NRI here.
+ assertEquals(1, nris.size.toLong())
+ assertTrue(nri.isMultilayerRequest)
+ assertEquals(nri.uids, uidRangesForUids(uids))
+ assertEquals(PREFERENCE_ORDER_SATELLITE_FALLBACK, nri.mPreferenceOrder)
+ }
+
+ private fun setAndUpdateSatelliteNetworkPreferredUids(uids: Set<Int>) {
+ visibleOnHandlerThread(csHandler) {
+ deps.satelliteNetworkFallbackUidUpdate!!.accept(uids)
+ }
+ }
+
+ private fun nativeNetworkConfigPhysical(netId: Int, permission: Int) =
+ NativeNetworkConfig(netId, NativeNetworkType.PHYSICAL, permission,
+ false /* secure */, VpnManager.TYPE_VPN_NONE, false /* excludeLocalRoutes */)
+
+ private fun createSatelliteAgent(name: String): CSAgentWrapper {
+ return Agent(score = keepScore(), lp = lp(name),
+ nc = nc(TRANSPORT_SATELLITE, NET_CAPABILITY_INTERNET)
+ )
+ }
+
+ private fun toUidRangeStableParcels(ranges: Set<UidRange>): Array<UidRangeParcel?> {
+ val stableRanges = arrayOfNulls<UidRangeParcel>(ranges.size)
+ for ((index, range) in ranges.withIndex()) {
+ stableRanges[index] = UidRangeParcel(range.start, range.stop)
+ }
+ return stableRanges
+ }
+
+ private fun uidRangesForUids(vararg uids: Int): Set<UidRange> {
+ val ranges = ArraySet<UidRange>()
+ for (uid in uids) {
+ ranges.add(UidRange(uid, uid))
+ }
+ return ranges
+ }
+
+ private fun uidRangesForUids(uids: Collection<Int>): Set<UidRange> {
+ return uidRangesForUids(*CollectionUtils.toIntArray(uids))
+ }
+
+ private fun nc(transport: Int, vararg caps: Int) = NetworkCapabilities.Builder().apply {
+ addTransportType(transport)
+ caps.forEach {
+ addCapability(it)
+ }
+ // Useful capabilities for everybody
+ addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ addCapability(NET_CAPABILITY_NOT_ROAMING)
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ }.build()
+
+ private fun lp(iface: String) = LinkProperties().apply {
+ interfaceName = iface
+ addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+ addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+ }
+
+ // This allows keeping all the networks connected without having to file individual requests
+ // for them.
+ private fun keepScore() = FromS(
+ NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build()
+ )
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index d41c742..13c5cbc 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -19,6 +19,8 @@
import android.content.Context
import android.net.ConnectivityManager
import android.net.INetworkMonitor
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS
+import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP
import android.net.INetworkMonitorCallbacks
import android.net.LinkProperties
import android.net.LocalNetworkConfig
@@ -26,6 +28,7 @@
import android.net.NetworkAgent
import android.net.NetworkAgentConfig
import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkProvider
@@ -37,6 +40,9 @@
import com.android.testutils.RecorderCallback.CallbackEntry.Available
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
import com.android.testutils.TestableNetworkCallback
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.test.assertEquals
+import kotlin.test.fail
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
@@ -44,9 +50,6 @@
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.verify
import org.mockito.stubbing.Answer
-import java.util.concurrent.atomic.AtomicInteger
-import kotlin.test.assertEquals
-import kotlin.test.fail
const val SHORT_TIMEOUT_MS = 200L
@@ -75,10 +78,15 @@
) : TestableNetworkCallback.HasNetwork {
private val TAG = "CSAgent${nextAgentId()}"
private val VALIDATION_RESULT_INVALID = 0
+ private val NO_PROBE_RESULT = 0
private val VALIDATION_TIMESTAMP = 1234L
private val agent: NetworkAgent
private val nmCallbacks: INetworkMonitorCallbacks
val networkMonitor = mock<INetworkMonitor>()
+ private var nmValidationRedirectUrl: String? = null
+ private var nmValidationResult = NO_PROBE_RESULT
+ private var nmProbesCompleted = NO_PROBE_RESULT
+ private var nmProbesSucceeded = NO_PROBE_RESULT
override val network: Network get() = agent.network!!
@@ -120,10 +128,10 @@
}
nmCallbacks.notifyProbeStatusChanged(0 /* completed */, 0 /* succeeded */)
val p = NetworkTestResultParcelable()
- p.result = VALIDATION_RESULT_INVALID
- p.probesAttempted = 0
- p.probesSucceeded = 0
- p.redirectUrl = null
+ p.result = nmValidationResult
+ p.probesAttempted = nmProbesCompleted
+ p.probesSucceeded = nmProbesSucceeded
+ p.redirectUrl = nmValidationRedirectUrl
p.timestampMillis = VALIDATION_TIMESTAMP
nmCallbacks.notifyNetworkTestedWithExtras(p)
}
@@ -133,6 +141,9 @@
val request = NetworkRequest.Builder().apply {
clearCapabilities()
if (nc.transportTypes.isNotEmpty()) addTransportType(nc.transportTypes[0])
+ if (nc.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) {
+ addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ }
}.build()
val cb = TestableNetworkCallback()
mgr.registerNetworkCallback(request, cb)
@@ -159,6 +170,9 @@
val request = NetworkRequest.Builder().apply {
clearCapabilities()
if (nc.transportTypes.isNotEmpty()) addTransportType(nc.transportTypes[0])
+ if (nc.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) {
+ addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ }
}.build()
val cb = TestableNetworkCallback(timeoutMs = SHORT_TIMEOUT_MS)
mgr.registerNetworkCallback(request, cb)
@@ -171,4 +185,27 @@
fun sendLocalNetworkConfig(lnc: LocalNetworkConfig) = agent.sendLocalNetworkConfig(lnc)
fun sendNetworkCapabilities(nc: NetworkCapabilities) = agent.sendNetworkCapabilities(nc)
+ fun sendLinkProperties(lp: LinkProperties) = agent.sendLinkProperties(lp)
+
+ fun connectWithCaptivePortal(redirectUrl: String) {
+ setCaptivePortal(redirectUrl)
+ connect()
+ }
+
+ fun setProbesStatus(probesCompleted: Int, probesSucceeded: Int) {
+ nmProbesCompleted = probesCompleted
+ nmProbesSucceeded = probesSucceeded
+ }
+
+ fun setCaptivePortal(redirectUrl: String) {
+ nmValidationResult = VALIDATION_RESULT_INVALID
+ nmValidationRedirectUrl = redirectUrl
+ // Suppose the portal is found when NetworkMonitor probes NETWORK_VALIDATION_PROBE_HTTP
+ // in the beginning. Because NETWORK_VALIDATION_PROBE_HTTP is the decisive probe for captive
+ // portal, considering the NETWORK_VALIDATION_PROBE_HTTPS hasn't probed yet and set only
+ // DNS and HTTP probes completed.
+ setProbesStatus(
+ NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTP /* probesCompleted */,
+ VALIDATION_RESULT_INVALID /* probesSucceeded */)
+ }
}
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 5c9a762..3b06ad0 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -16,6 +16,8 @@
package com.android.server
+import android.app.AlarmManager
+import android.app.AppOpsManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -41,20 +43,22 @@
import android.net.NetworkProvider
import android.net.NetworkScore
import android.net.PacProxyManager
+import android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_LOCAL_NETWORK
import android.net.networkstack.NetworkStackClientBase
import android.os.BatteryStatsManager
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
+import android.os.Process
import android.os.UserHandle
import android.os.UserManager
+import android.permission.PermissionManager.PermissionResult
+import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import android.testing.TestableContext
-import android.util.ArraySet
import androidx.test.platform.app.InstrumentationRegistry
import com.android.internal.app.IBatteryStats
import com.android.internal.util.test.BroadcastInterceptingContext
-import com.android.metrics.NetworkRequestStateStatsMetrics
import com.android.modules.utils.build.SdkLevel
import com.android.net.module.util.ArrayTrackRecord
import com.android.networkstack.apishim.common.UnsupportedApiLevelException
@@ -62,20 +66,34 @@
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.SatelliteAccessController
import com.android.testutils.visibleOnHandlerThread
import com.android.testutils.waitForIdle
import java.util.concurrent.Executors
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.TimeUnit
+import java.util.function.BiConsumer
+import java.util.function.Consumer
+import kotlin.annotation.AnnotationRetention.RUNTIME
+import kotlin.annotation.AnnotationTarget.FUNCTION
+import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.fail
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.rules.TestName
import org.mockito.AdditionalAnswers.delegatesTo
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
-internal const val HANDLER_TIMEOUT_MS = 2_000
+internal const val HANDLER_TIMEOUT_MS = 2_000L
internal const val BROADCAST_TIMEOUT_MS = 3_000L
internal const val TEST_PACKAGE_NAME = "com.android.test.package"
internal const val WIFI_WOL_IFNAME = "test_wlan_wol"
@@ -91,6 +109,8 @@
internal const val VERSION_V = 5
internal const val VERSION_MAX = VERSION_V
+internal const val CALLING_UID_UNMOCKED = Process.INVALID_UID
+
private fun NetworkCapabilities.getLegacyType() =
when (transportTypes.getOrElse(0) { TRANSPORT_WIFI }) {
TRANSPORT_BLUETOOTH -> ConnectivityManager.TYPE_BLUETOOTH
@@ -110,14 +130,19 @@
// TODO (b/272685721) : make ConnectivityServiceTest smaller and faster by moving the setup
// parts into this class and moving the individual tests to multiple separate classes.
open class CSTest {
+ @get:Rule
+ val testNameRule = TestName()
+
companion object {
val CSTestExecutor = Executors.newSingleThreadExecutor()
}
init {
if (!SdkLevel.isAtLeastS()) {
- throw UnsupportedApiLevelException("CSTest subclasses must be annotated to only " +
- "run on S+, e.g. @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)")
+ throw UnsupportedApiLevelException(
+ "CSTest subclasses must be annotated to only " +
+ "run on S+, e.g. @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)"
+ )
}
}
@@ -130,13 +155,15 @@
// permissions using static contexts.
val enabledFeatures = HashMap<String, Boolean>().also {
it[ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER] = true
+ it[ConnectivityFlags.REQUEST_RESTRICTED_WIFI] = true
it[ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION] = true
it[ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION] = true
it[ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS] = true
- it[ConnectivityService.LOG_BPF_RC] = true
+ it[ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK] = true
+ it[ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING] = true
+ it[ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN] = true
}
- fun enableFeature(f: String) = enabledFeatures.set(f, true)
- fun disableFeature(f: String) = enabledFeatures.set(f, false)
+ fun setFeatureEnabled(flag: String, enabled: Boolean) = enabledFeatures.set(flag, enabled)
// When adding new members, consider if it's not better to build the object in CSTestHelpers
// to keep this file clean of implementation details. Generally, CSTestHelpers should only
@@ -160,18 +187,69 @@
val clatCoordinator = mock<ClatCoordinator>()
val networkRequestStateStatsMetrics = mock<NetworkRequestStateStatsMetrics>()
val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
- val alarmManager = makeMockAlarmManager()
val systemConfigManager = makeMockSystemConfigManager()
val batteryStats = mock<IBatteryStats>()
val batteryManager = BatteryStatsManager(batteryStats)
+ val appOpsManager = mock<AppOpsManager>()
val telephonyManager = mock<TelephonyManager>().also {
doReturn(true).`when`(it).isDataCapable()
}
+ val subscriptionManager = mock<SubscriptionManager>()
+
+ val multicastRoutingCoordinatorService = mock<MulticastRoutingCoordinatorService>()
+ val satelliteAccessController = mock<SatelliteAccessController>()
val deps = CSDeps()
- val service = makeConnectivityService(context, netd, deps).also { it.systemReadyInternal() }
- val cm = ConnectivityManager(context, service)
- val csHandler = Handler(csHandlerThread.looper)
+
+ // Initializations that start threads are done from setUp to avoid thread leak
+ lateinit var alarmHandlerThread: HandlerThread
+ lateinit var alarmManager: AlarmManager
+ lateinit var service: ConnectivityService
+ lateinit var cm: ConnectivityManager
+ lateinit var csHandler: Handler
+
+ // Tests can use this annotation to set flag values before constructing ConnectivityService
+ // e.g. @FeatureFlags([Flag(flagName1, true/false), Flag(flagName2, true/false)])
+ @Retention(RUNTIME)
+ @Target(FUNCTION)
+ annotation class FeatureFlags(val flags: Array<Flag>)
+
+ @Retention(RUNTIME)
+ @Target(FUNCTION)
+ annotation class Flag(val name: String, val enabled: Boolean)
+
+ @Before
+ fun setUp() {
+ // Set feature flags before constructing ConnectivityService
+ val testMethodName = testNameRule.methodName
+ try {
+ val testMethod = this::class.java.getMethod(testMethodName)
+ val featureFlags = testMethod.getAnnotation(FeatureFlags::class.java)
+ if (featureFlags != null) {
+ for (flag in featureFlags.flags) {
+ setFeatureEnabled(flag.name, flag.enabled)
+ }
+ }
+ } catch (ignored: NoSuchMethodException) {
+ // This is expected for parameterized tests
+ }
+
+ alarmHandlerThread = HandlerThread("TestAlarmManager").also { it.start() }
+ alarmManager = makeMockAlarmManager(alarmHandlerThread)
+ service = makeConnectivityService(context, netd, deps).also { it.systemReadyInternal() }
+ cm = ConnectivityManager(context, service)
+ // csHandler initialization must be after makeConnectivityService since ConnectivityService
+ // constructor starts csHandlerThread
+ csHandler = Handler(csHandlerThread.looper)
+ }
+
+ @After
+ fun tearDown() {
+ csHandlerThread.quitSafely()
+ csHandlerThread.join()
+ alarmHandlerThread.quitSafely()
+ alarmHandlerThread.join()
+ }
inner class CSDeps : ConnectivityService.Dependencies() {
override fun getResources(ctx: Context) = connResources
@@ -181,12 +259,27 @@
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,
- tm: TelephonyManager
+ tm: TelephonyManager,
+ requestRestrictedWifiEnabled: Boolean,
+ listener: BiConsumer<Int, Int>,
+ handler: Handler
) = if (SdkLevel.isAtLeastT()) mock<CarrierPrivilegeAuthenticator>() else null
+ var satelliteNetworkFallbackUidUpdate: Consumer<Set<Int>>? = null
+ override fun makeSatelliteAccessController(
+ context: Context,
+ updateSatelliteNetworkFallackUid: Consumer<Set<Int>>?,
+ csHandlerThread: Handler
+ ): SatelliteAccessController? {
+ satelliteNetworkFallbackUidUpdate = updateSatelliteNetworkFallackUid
+ return satelliteAccessController
+ }
+
private inner class AOOKTDeps(c: Context) : AutomaticOnOffKeepaliveTracker.Dependencies(c) {
override fun isTetheringFeatureNotChickenedOut(name: String): Boolean {
return isFeatureEnabled(context, name)
@@ -196,8 +289,12 @@
AutomaticOnOffKeepaliveTracker(c, h, AOOKTDeps(c))
override fun makeMultinetworkPolicyTracker(c: Context, h: Handler, r: Runnable) =
- MultinetworkPolicyTracker(c, h, r,
- MultinetworkPolicyTrackerTestDependencies(connResources.get()))
+ MultinetworkPolicyTracker(
+ c,
+ h,
+ r,
+ MultinetworkPolicyTrackerTestDependencies(connResources.get())
+ )
override fun makeNetworkRequestStateStatsMetrics(c: Context) =
this@CSTest.networkRequestStateStatsMetrics
@@ -211,7 +308,7 @@
enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures")
// Mocked change IDs
- private val enabledChangeIds = ArraySet<Long>()
+ private val enabledChangeIds = arrayListOf(ENABLE_MATCH_LOCAL_NETWORK)
fun setChangeIdEnabled(enabled: Boolean, changeId: Long) {
// enabledChangeIds is read on the handler thread and maybe the test thread, so
// make sure both threads see it before continuing.
@@ -246,19 +343,92 @@
override fun isAtLeastT() = if (isSdkUnmocked) super.isAtLeastT() else sdkLevel >= VERSION_T
override fun isAtLeastU() = if (isSdkUnmocked) super.isAtLeastU() else sdkLevel >= VERSION_U
override fun isAtLeastV() = if (isSdkUnmocked) super.isAtLeastV() else sdkLevel >= VERSION_V
+
+ private var callingUid = CALLING_UID_UNMOCKED
+
+ fun unmockCallingUid() {
+ setCallingUid(CALLING_UID_UNMOCKED)
+ }
+
+ fun setCallingUid(callingUid: Int) {
+ visibleOnHandlerThread(csHandler) { this.callingUid = callingUid }
+ }
+
+ override fun getCallingUid() =
+ if (callingUid == CALLING_UID_UNMOCKED) super.getCallingUid() else callingUid
}
inner class CSContext(base: Context) : BroadcastInterceptingContext(base) {
val pacProxyManager = mock<PacProxyManager>()
val networkPolicyManager = mock<NetworkPolicyManager>()
+ // Map of permission name -> PermissionManager.Permission_{GRANTED|DENIED} constant
+ // For permissions granted across the board, the key is only the permission name.
+ // For permissions only granted to a combination of uid/pid, the key
+ // is "<permission name>,<pid>,<uid>". PID+UID permissions have priority over generic ones.
+ private val mMockedPermissions: HashMap<String, Int> = HashMap()
+ private val mStartedActivities = LinkedBlockingQueue<Intent>()
override fun getPackageManager() = this@CSTest.packageManager
override fun getContentResolver() = this@CSTest.contentResolver
- // TODO : buff up the capabilities of this permission scheme to allow checking for
- // permission rejections
- override fun checkPermission(permission: String, pid: Int, uid: Int) = PERMISSION_GRANTED
- override fun checkCallingOrSelfPermission(permission: String) = PERMISSION_GRANTED
+ // If the permission result does not set in the mMockedPermissions, it will be
+ // considered as PERMISSION_GRANTED as existing design to prevent breaking other tests.
+ override fun checkPermission(permission: String, pid: Int, uid: Int) =
+ checkMockedPermission(permission, pid, uid, PERMISSION_GRANTED)
+
+ override fun enforceCallingOrSelfPermission(permission: String, message: String?) {
+ // If the permission result does not set in the mMockedPermissions, it will be
+ // considered as PERMISSION_GRANTED as existing design to prevent breaking other tests.
+ val granted = checkMockedPermission(
+ permission,
+ Process.myPid(),
+ Process.myUid(),
+ PERMISSION_GRANTED
+ )
+ if (!granted.equals(PERMISSION_GRANTED)) {
+ throw SecurityException("[Test] permission denied: " + permission)
+ }
+ }
+
+ // If the permission result does not set in the mMockedPermissions, it will be
+ // considered as PERMISSION_GRANTED as existing design to prevent breaking other tests.
+ override fun checkCallingOrSelfPermission(permission: String) =
+ checkMockedPermission(permission, Process.myPid(), Process.myUid(), PERMISSION_GRANTED)
+
+ private fun checkMockedPermission(
+ permission: String,
+ pid: Int,
+ uid: Int,
+ default: Int
+ ): Int {
+ val processSpecificKey = "$permission,$pid,$uid"
+ return mMockedPermissions[processSpecificKey]
+ ?: mMockedPermissions[permission] ?: default
+ }
+
+ /**
+ * Mock checks for the specified permission, and have them behave as per `granted` or
+ * `denied`.
+ *
+ * This will apply to all calls no matter what the checked UID and PID are.
+ *
+ * @param granted One of {@link PackageManager#PermissionResult}.
+ */
+ fun setPermission(permission: String, @PermissionResult granted: Int) {
+ mMockedPermissions.put(permission, granted)
+ }
+
+ /**
+ * Mock checks for the specified permission, and have them behave as per `granted` or
+ * `denied`.
+ *
+ * This will only apply to the passed UID and PID.
+ *
+ * @param granted One of {@link PackageManager#PermissionResult}.
+ */
+ fun setPermission(permission: String, pid: Int, uid: Int, @PermissionResult granted: Int) {
+ mMockedPermissions.put("$permission,$pid,$uid", granted)
+ }
// Necessary for MultinetworkPolicyTracker, which tries to register a receiver for
// all users. The test can't do that since it doesn't hold INTERACT_ACROSS_USERS.
@@ -292,16 +462,17 @@
Context.ACTIVITY_SERVICE -> activityManager
Context.SYSTEM_CONFIG_SERVICE -> systemConfigManager
Context.TELEPHONY_SERVICE -> telephonyManager
+ Context.TELEPHONY_SUBSCRIPTION_SERVICE -> subscriptionManager
Context.BATTERY_STATS_SERVICE -> batteryManager
Context.STATS_MANAGER -> null // Stats manager is final and can't be mocked
+ Context.APP_OPS_SERVICE -> appOpsManager
else -> super.getSystemService(serviceName)
}
internal val orderedBroadcastAsUserHistory = ArrayTrackRecord<Intent>().newReadHead()
fun expectNoDataActivityBroadcast(timeoutMs: Int) {
- assertNull(orderedBroadcastAsUserHistory.poll(
- timeoutMs.toLong()) { intent -> true })
+ assertNull(orderedBroadcastAsUserHistory.poll(timeoutMs.toLong()))
}
override fun sendOrderedBroadcastAsUser(
@@ -316,6 +487,16 @@
) {
orderedBroadcastAsUserHistory.add(intent)
}
+
+ override fun startActivityAsUser(intent: Intent, handle: UserHandle) {
+ mStartedActivities.put(intent)
+ }
+
+ fun expectStartActivityIntent(timeoutMs: Long = HANDLER_TIMEOUT_MS): Intent {
+ val intent = mStartedActivities.poll(timeoutMs, TimeUnit.MILLISECONDS)
+ assertNotNull(intent, "Did not receive sign-in intent after " + timeoutMs + "ms")
+ return intent
+ }
}
// Utility methods for subclasses to use
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/ethernet/EthernetNetworkFactoryTest.java b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
index 949e0c2..70d4ad8 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -554,4 +554,22 @@
mNetworkOfferCallback.onNetworkNeeded(createDefaultRequest());
verify(mIpClient, never()).startProvisioning(any());
}
+
+ @Test
+ public void testGetMacAddressProvisionedInterface() throws Exception {
+ initEthernetNetworkFactory();
+ createAndVerifyProvisionedInterface(TEST_IFACE);
+
+ final String result = mNetFactory.getHwAddress(TEST_IFACE);
+ assertEquals(HW_ADDR, result);
+ }
+
+ @Test
+ public void testGetMacAddressForNonExistingInterface() {
+ initEthernetNetworkFactory();
+
+ final String result = mNetFactory.getHwAddress(TEST_IFACE);
+ // No interface exists due to not calling createAndVerifyProvisionedInterface(...).
+ assertNull(result);
+ }
}
diff --git a/tests/unit/java/com/android/server/net/BpfInterfaceMapHelperTest.java b/tests/unit/java/com/android/server/net/BpfInterfaceMapHelperTest.java
new file mode 100644
index 0000000..7b3bea3
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/BpfInterfaceMapHelperTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static android.system.OsConstants.EPERM;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
+
+import android.os.Build;
+import android.system.ErrnoException;
+import android.util.IndentingPrintWriter;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct.S32;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.TestBpfMap;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+@SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+public final class BpfInterfaceMapHelperTest {
+ private static final int TEST_INDEX = 1;
+ private static final int TEST_INDEX2 = 2;
+ private static final String TEST_INTERFACE_NAME = "test1";
+ private static final String TEST_INTERFACE_NAME2 = "test2";
+
+ private BaseNetdUnsolicitedEventListener mListener;
+ private BpfInterfaceMapHelper mUpdater;
+ private IBpfMap<S32, InterfaceMapValue> mBpfMap =
+ spy(new TestBpfMap<>(S32.class, InterfaceMapValue.class));
+
+ private class TestDependencies extends BpfInterfaceMapHelper.Dependencies {
+ @Override
+ public IBpfMap<S32, InterfaceMapValue> getInterfaceMap() {
+ return mBpfMap;
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mUpdater = new BpfInterfaceMapHelper(new TestDependencies());
+ }
+
+ @Test
+ public void testGetIfNameByIndex() throws Exception {
+ mBpfMap.updateEntry(new S32(TEST_INDEX), new InterfaceMapValue(TEST_INTERFACE_NAME));
+ assertEquals(TEST_INTERFACE_NAME, mUpdater.getIfNameByIndex(TEST_INDEX));
+ }
+
+ @Test
+ public void testGetIfNameByIndexNoEntry() {
+ assertNull(mUpdater.getIfNameByIndex(TEST_INDEX));
+ }
+
+ @Test
+ public void testGetIfNameByIndexException() throws Exception {
+ doThrow(new ErrnoException("", EPERM)).when(mBpfMap).getValue(new S32(TEST_INDEX));
+ assertNull(mUpdater.getIfNameByIndex(TEST_INDEX));
+ }
+
+ private void assertDumpContains(final String dump, final String message) {
+ assertTrue(String.format("dump(%s) does not contain '%s'", dump, message),
+ dump.contains(message));
+ }
+
+ private String getDump() {
+ final StringWriter sw = new StringWriter();
+ mUpdater.dump(new IndentingPrintWriter(new PrintWriter(sw), " "));
+ return sw.toString();
+ }
+
+ @Test
+ public void testDump() throws ErrnoException {
+ mBpfMap.updateEntry(new S32(TEST_INDEX), new InterfaceMapValue(TEST_INTERFACE_NAME));
+ mBpfMap.updateEntry(new S32(TEST_INDEX2), new InterfaceMapValue(TEST_INTERFACE_NAME2));
+
+ final String dump = getDump();
+ assertDumpContains(dump, "IfaceIndexNameMap: OK");
+ assertDumpContains(dump, "ifaceIndex=1 ifaceName=test1");
+ assertDumpContains(dump, "ifaceIndex=2 ifaceName=test2");
+ }
+}
diff --git a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
deleted file mode 100644
index c730856..0000000
--- a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.net;
-
-import static android.system.OsConstants.EPERM;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.net.INetd;
-import android.net.MacAddress;
-import android.os.Build;
-import android.os.Handler;
-import android.os.test.TestLooper;
-import android.system.ErrnoException;
-import android.util.IndentingPrintWriter;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
-import com.android.net.module.util.IBpfMap;
-import com.android.net.module.util.InterfaceParams;
-import com.android.net.module.util.Struct.S32;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-import com.android.testutils.TestBpfMap;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-
-@SmallTest
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
-public final class BpfInterfaceMapUpdaterTest {
- private static final int TEST_INDEX = 1;
- private static final int TEST_INDEX2 = 2;
- private static final String TEST_INTERFACE_NAME = "test1";
- private static final String TEST_INTERFACE_NAME2 = "test2";
-
- private final TestLooper mLooper = new TestLooper();
- private BaseNetdUnsolicitedEventListener mListener;
- private BpfInterfaceMapUpdater mUpdater;
- private IBpfMap<S32, InterfaceMapValue> mBpfMap =
- spy(new TestBpfMap<>(S32.class, InterfaceMapValue.class));
- @Mock private INetd mNetd;
- @Mock private Context mContext;
-
- private class TestDependencies extends BpfInterfaceMapUpdater.Dependencies {
- @Override
- public IBpfMap<S32, InterfaceMapValue> getInterfaceMap() {
- return mBpfMap;
- }
-
- @Override
- public InterfaceParams getInterfaceParams(String ifaceName) {
- if (ifaceName.equals(TEST_INTERFACE_NAME)) {
- return new InterfaceParams(TEST_INTERFACE_NAME, TEST_INDEX,
- MacAddress.ALL_ZEROS_ADDRESS);
- } else if (ifaceName.equals(TEST_INTERFACE_NAME2)) {
- return new InterfaceParams(TEST_INTERFACE_NAME2, TEST_INDEX2,
- MacAddress.ALL_ZEROS_ADDRESS);
- }
-
- return null;
- }
-
- @Override
- public INetd getINetd(Context ctx) {
- return mNetd;
- }
- }
-
- @Before
- public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
- when(mNetd.interfaceGetList()).thenReturn(new String[] {TEST_INTERFACE_NAME});
- mUpdater = new BpfInterfaceMapUpdater(mContext, new Handler(mLooper.getLooper()),
- new TestDependencies());
- }
-
- private void verifyStartUpdater() throws Exception {
- mUpdater.start();
- mLooper.dispatchAll();
- final ArgumentCaptor<BaseNetdUnsolicitedEventListener> listenerCaptor =
- ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener.class);
- verify(mNetd).registerUnsolicitedEventListener(listenerCaptor.capture());
- mListener = listenerCaptor.getValue();
- verify(mBpfMap).updateEntry(eq(new S32(TEST_INDEX)),
- eq(new InterfaceMapValue(TEST_INTERFACE_NAME)));
- }
-
- @Test
- public void testUpdateInterfaceMap() throws Exception {
- verifyStartUpdater();
-
- mListener.onInterfaceAdded(TEST_INTERFACE_NAME2);
- mLooper.dispatchAll();
- verify(mBpfMap).updateEntry(eq(new S32(TEST_INDEX2)),
- eq(new InterfaceMapValue(TEST_INTERFACE_NAME2)));
-
- // Check that when onInterfaceRemoved is called, nothing happens.
- mListener.onInterfaceRemoved(TEST_INTERFACE_NAME);
- mLooper.dispatchAll();
- verifyNoMoreInteractions(mBpfMap);
- }
-
- @Test
- public void testGetIfNameByIndex() throws Exception {
- mBpfMap.updateEntry(new S32(TEST_INDEX), new InterfaceMapValue(TEST_INTERFACE_NAME));
- assertEquals(TEST_INTERFACE_NAME, mUpdater.getIfNameByIndex(TEST_INDEX));
- }
-
- @Test
- public void testGetIfNameByIndexNoEntry() {
- assertNull(mUpdater.getIfNameByIndex(TEST_INDEX));
- }
-
- @Test
- public void testGetIfNameByIndexException() throws Exception {
- doThrow(new ErrnoException("", EPERM)).when(mBpfMap).getValue(new S32(TEST_INDEX));
- assertNull(mUpdater.getIfNameByIndex(TEST_INDEX));
- }
-
- private void assertDumpContains(final String dump, final String message) {
- assertTrue(String.format("dump(%s) does not contain '%s'", dump, message),
- dump.contains(message));
- }
-
- private String getDump() {
- final StringWriter sw = new StringWriter();
- mUpdater.dump(new IndentingPrintWriter(new PrintWriter(sw), " "));
- return sw.toString();
- }
-
- @Test
- public void testDump() throws ErrnoException {
- mBpfMap.updateEntry(new S32(TEST_INDEX), new InterfaceMapValue(TEST_INTERFACE_NAME));
- mBpfMap.updateEntry(new S32(TEST_INDEX2), new InterfaceMapValue(TEST_INTERFACE_NAME2));
-
- final String dump = getDump();
- assertDumpContains(dump, "IfaceIndexNameMap: OK");
- assertDumpContains(dump, "ifaceIndex=1 ifaceName=test1");
- assertDumpContains(dump, "ifaceIndex=2 ifaceName=test2");
- }
-}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 1ee3f9d..8ceca9a 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -64,6 +64,7 @@
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.WEEK_IN_MILLIS;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow;
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;
@@ -85,6 +86,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;
@@ -97,6 +99,7 @@
import android.app.AlarmManager;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.net.DataUsageRequest;
@@ -124,12 +127,15 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.PowerManager;
+import android.os.Process;
import android.os.SimpleClock;
+import android.os.UserHandle;
import android.provider.Settings;
import android.system.ErrnoException;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.IndentingPrintWriter;
import android.util.Pair;
import androidx.annotation.Nullable;
@@ -243,6 +249,7 @@
private static @Mock WifiInfo sWifiInfo;
private @Mock INetd mNetd;
private @Mock TetheringManager mTetheringManager;
+ private @Mock PackageManager mPm;
private @Mock NetworkStatsFactory mStatsFactory;
@NonNull
private final TestNetworkStatsSettings mSettings =
@@ -252,7 +259,7 @@
private @Mock AlarmManager mAlarmManager;
@Mock
private NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor;
- private @Mock BpfInterfaceMapUpdater mBpfInterfaceMapUpdater;
+ private @Mock BpfInterfaceMapHelper mBpfInterfaceMapHelper;
private HandlerThread mHandlerThread;
@Mock
private LocationPermissionChecker mLocationPermissionChecker;
@@ -303,6 +310,16 @@
}
@Override
+ public PackageManager getPackageManager() {
+ return mPm;
+ }
+
+ @Override
+ public Context createContextAsUser(UserHandle user, int flags) {
+ return this;
+ }
+
+ @Override
public Object getSystemService(String name) {
if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager;
if (Context.TETHERING_SERVICE.equals(name)) return mTetheringManager;
@@ -425,6 +442,9 @@
any(), tetheringEventCbCaptor.capture());
mTetheringEventCallback = tetheringEventCbCaptor.getValue();
+ doReturn(Process.myUid()).when(mPm)
+ .getPackageUid(eq(mServiceContext.getPackageName()), anyInt());
+
mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
}
@@ -517,9 +537,8 @@
}
@Override
- public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
- @NonNull Context ctx, @NonNull Handler handler) {
- return mBpfInterfaceMapUpdater;
+ public BpfInterfaceMapHelper makeBpfInterfaceMapHelper() {
+ return mBpfInterfaceMapHelper;
}
@Override
@@ -928,7 +947,16 @@
}
@Test
- public void testMobileStatsByRatType() throws Exception {
+ public void testMobileStatsByRatTypeForSatellite() throws Exception {
+ doTestMobileStatsByRatType(new NetworkStateSnapshot[]{buildSatelliteMobileState(IMSI_1)});
+ }
+
+ @Test
+ public void testMobileStatsByRatTypeForCellular() throws Exception {
+ doTestMobileStatsByRatType(new NetworkStateSnapshot[]{buildMobileState(IMSI_1)});
+ }
+
+ private void doTestMobileStatsByRatType(NetworkStateSnapshot[] states) throws Exception {
final NetworkTemplate template3g = new NetworkTemplate.Builder(MATCH_MOBILE)
.setRatType(TelephonyManager.NETWORK_TYPE_UMTS)
.setMeteredness(METERED_YES).build();
@@ -938,8 +966,6 @@
final NetworkTemplate template5g = new NetworkTemplate.Builder(MATCH_MOBILE)
.setRatType(TelephonyManager.NETWORK_TYPE_NR)
.setMeteredness(METERED_YES).build();
- final NetworkStateSnapshot[] states =
- new NetworkStateSnapshot[]{buildMobileState(IMSI_1)};
// 3G network comes online.
mockNetworkStatsSummary(buildEmptyStats());
@@ -953,7 +979,7 @@
incrementCurrentTime(MINUTE_IN_MILLIS);
mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
.addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L)));
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12L, 18L, 14L, 1L, 0L)));
forcePollAndWaitForIdle();
// Verify 3g templates gets stats.
@@ -968,7 +994,7 @@
mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
// Append more traffic on existing 3g stats entry.
.addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 16L, 22L, 17L, 2L, 0L))
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 16L, 22L, 17L, 2L, 0L))
// Add entry that is new on 4g.
.addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE,
METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 33L, 27L, 8L, 10L, 1L)));
@@ -1370,6 +1396,57 @@
}
@Test
+ public void testGetUidStatsForTransportWithCellularAndSatellite() throws Exception {
+ // Setup satellite mobile network and Cellular mobile network
+ mockDefaultSettings();
+ mockNetworkStatsUidDetail(buildEmptyStats());
+
+ final NetworkStateSnapshot mobileState = buildStateOfTransport(
+ NetworkCapabilities.TRANSPORT_CELLULAR, TYPE_MOBILE,
+ TEST_IFACE2, IMSI_1, null /* wifiNetworkKey */,
+ false /* isTemporarilyNotMetered */, false /* isRoaming */);
+
+ final NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{mobileState,
+ buildSatelliteMobileState(IMSI_1)};
+ mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
+ new UnderlyingNetworkInfo[0]);
+ setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_LTE);
+
+ // mock traffic on satellite network
+ final NetworkStats.Entry entrySatellite = new NetworkStats.Entry(
+ TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+ DEFAULT_NETWORK_NO, 80L, 5L, 70L, 15L, 1L);
+
+ // mock traffic on cellular network
+ final NetworkStats.Entry entryCellular = new NetworkStats.Entry(
+ TEST_IFACE2, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+ DEFAULT_NETWORK_NO, 100L, 15L, 150L, 15L, 1L);
+
+ final TetherStatsParcel[] emptyTetherStats = {};
+ // The interfaces that expect to be used to query the stats.
+ final String[] mobileIfaces = {TEST_IFACE, TEST_IFACE2};
+ incrementCurrentTime(HOUR_IN_MILLIS);
+ mockDefaultSettings();
+ mockNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+ .insertEntry(entrySatellite).insertEntry(entryCellular), emptyTetherStats,
+ mobileIfaces);
+ // with getUidStatsForTransport(TRANSPORT_CELLULAR) return stats of both cellular
+ // and satellite
+ final NetworkStats mobileStats = mService.getUidStatsForTransport(
+ NetworkCapabilities.TRANSPORT_CELLULAR);
+
+ // The iface field of the returned stats should be null because getUidStatsForTransport
+ // clears the interface field before it returns the result.
+ assertValues(mobileStats, null /* iface */, UID_RED, SET_DEFAULT, TAG_NONE,
+ METERED_NO, ROAMING_NO, METERED_NO, 180L, 20L, 220L, 30L, 2L);
+
+ // getUidStatsForTransport(TRANSPORT_SATELLITE) is not supported
+ assertThrows(IllegalArgumentException.class,
+ () -> mService.getUidStatsForTransport(NetworkCapabilities.TRANSPORT_SATELLITE));
+
+ }
+
+ @Test
public void testForegroundBackground() throws Exception {
// pretend that network comes online
mockDefaultSettings();
@@ -1591,7 +1668,7 @@
// Register and verify request and that binder was called
DataUsageRequest request = mService.registerUsageCallback(
- mServiceContext.getOpPackageName(), inputRequest, mUsageCallback);
+ mServiceContext.getPackageName(), inputRequest, mUsageCallback);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateWifi, request.template));
long minThresholdInBytes = 2 * 1024 * 1024; // 2 MB
@@ -2527,6 +2604,12 @@
false /* isTemporarilyNotMetered */, false /* isRoaming */);
}
+ private static NetworkStateSnapshot buildSatelliteMobileState(String subscriberId) {
+ return buildStateOfTransport(NetworkCapabilities.TRANSPORT_SATELLITE, TYPE_MOBILE,
+ TEST_IFACE, subscriberId, null /* wifiNetworkKey */,
+ false /* isTemporarilyNotMetered */, false /* isRoaming */);
+ }
+
private static NetworkStateSnapshot buildTestState(@NonNull String iface,
@Nullable String wifiNetworkKey) {
return buildStateOfTransport(NetworkCapabilities.TRANSPORT_TEST, TYPE_TEST,
@@ -2762,13 +2845,13 @@
@Test
public void testDumpStatsMap() throws ErrnoException {
- doReturn("wlan0").when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
+ doReturn("wlan0").when(mBpfInterfaceMapHelper).getIfNameByIndex(10 /* index */);
doTestDumpStatsMap("wlan0");
}
@Test
public void testDumpStatsMapUnknownInterface() throws ErrnoException {
- doReturn(null).when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
+ doReturn(null).when(mBpfInterfaceMapHelper).getIfNameByIndex(10 /* index */);
doTestDumpStatsMap("unknown");
}
@@ -2783,13 +2866,13 @@
@Test
public void testDumpIfaceStatsMap() throws Exception {
- doReturn("wlan0").when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
+ doReturn("wlan0").when(mBpfInterfaceMapHelper).getIfNameByIndex(10 /* index */);
doTestDumpIfaceStatsMap("wlan0");
}
@Test
public void testDumpIfaceStatsMapUnknownInterface() throws Exception {
- doReturn(null).when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
+ doReturn(null).when(mBpfInterfaceMapHelper).getIfNameByIndex(10 /* index */);
doTestDumpIfaceStatsMap("unknown");
}
@@ -2802,4 +2885,48 @@
final String dump = getDump();
assertDumpContains(dump, pollReasonNameOf(POLL_REASON_RAT_CHANGED));
}
+
+ @Test
+ public void testEnforcePackageNameMatchesUid() throws Exception {
+ final String testMyPackageName = "test.package.myname";
+ final String testRedPackageName = "test.package.red";
+ final String testInvalidPackageName = "test.package.notfound";
+
+ doReturn(UID_RED).when(mPm).getPackageUid(eq(testRedPackageName), anyInt());
+ doReturn(Process.myUid()).when(mPm).getPackageUid(eq(testMyPackageName), anyInt());
+ doThrow(new PackageManager.NameNotFoundException()).when(mPm)
+ .getPackageUid(eq(testInvalidPackageName), anyInt());
+
+ assertThrows(SecurityException.class, () ->
+ mService.openSessionForUsageStats(0 /* flags */, testRedPackageName));
+ assertThrows(SecurityException.class, () ->
+ mService.openSessionForUsageStats(0 /* flags */, testInvalidPackageName));
+ assertThrows(NullPointerException.class, () ->
+ mService.openSessionForUsageStats(0 /* flags */, null));
+ // Verify package name belongs to ourselves does not throw.
+ mService.openSessionForUsageStats(0 /* flags */, testMyPackageName);
+
+ long thresholdInBytes = 10 * 1024 * 1024; // 10 MB
+ DataUsageRequest request = new DataUsageRequest(
+ 2 /* requestId */, sTemplateImsi1, thresholdInBytes);
+ assertThrows(SecurityException.class, () ->
+ mService.registerUsageCallback(testRedPackageName, request, mUsageCallback));
+ assertThrows(SecurityException.class, () ->
+ mService.registerUsageCallback(testInvalidPackageName, request, mUsageCallback));
+ assertThrows(NullPointerException.class, () ->
+ mService.registerUsageCallback(null, request, mUsageCallback));
+ mService.registerUsageCallback(testMyPackageName, request, mUsageCallback);
+ }
+
+ @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/tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt b/tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt
new file mode 100644
index 0000000..18785e5
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/SkDestroyListenerTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.os.Handler
+import android.os.HandlerThread
+import com.android.net.module.util.SharedLog
+import com.android.testutils.DevSdkIgnoreRunner
+import java.io.PrintWriter
+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.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+class SkDestroyListenerTest {
+ @Mock lateinit var sharedLog: SharedLog
+ val handlerThread = HandlerThread("SkDestroyListenerTest")
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ handlerThread.start()
+ }
+
+ @After
+ fun tearDown() {
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ @Test
+ fun testDump() {
+ doReturn(sharedLog).`when`(sharedLog).forSubComponent(any())
+
+ val handler = Handler(handlerThread.looper)
+ val skDestroylistener = SkDestroyListener(null /* cookieTagMap */, handler, sharedLog)
+ val pw = PrintWriter(System.out)
+ skDestroylistener.dump(pw)
+
+ verify(sharedLog).reverseDump(pw)
+ }
+}
diff --git a/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
new file mode 100644
index 0000000..99f762d
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.net.NetworkStats.Entry
+import com.android.testutils.DevSdkIgnoreRunner
+import java.time.Clock
+import java.util.function.Supplier
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+@RunWith(DevSdkIgnoreRunner::class)
+class TrafficStatsRateLimitCacheTest {
+ companion object {
+ private const val expiryDurationMs = 1000L
+ private const val maxSize = 2
+ }
+
+ private val clock = mock(Clock::class.java)
+ private val entry = mock(Entry::class.java)
+ private val cache = TrafficStatsRateLimitCache(clock, expiryDurationMs, maxSize)
+
+ @Test
+ fun testGet_returnsEntryIfNotExpired() {
+ cache.put("iface", 2, entry)
+ doReturn(500L).`when`(clock).millis() // Set clock to before expiry
+ val result = cache.get("iface", 2)
+ assertEquals(entry, result)
+ }
+
+ @Test
+ fun testGet_returnsNullIfExpired() {
+ cache.put("iface", 2, entry)
+ doReturn(2000L).`when`(clock).millis() // Set clock to after expiry
+ assertNull(cache.get("iface", 2))
+ }
+
+ @Test
+ fun testGet_returnsNullForNonExistentKey() {
+ val result = cache.get("otherIface", 99)
+ assertNull(result)
+ }
+
+ @Test
+ fun testPutAndGet_retrievesCorrectEntryForDifferentKeys() {
+ val entry1 = mock(Entry::class.java)
+ val entry2 = mock(Entry::class.java)
+
+ cache.put("iface1", 2, entry1)
+ cache.put("iface2", 4, entry2)
+
+ assertEquals(entry1, cache.get("iface1", 2))
+ assertEquals(entry2, cache.get("iface2", 4))
+ }
+
+ @Test
+ fun testPut_overridesExistingEntry() {
+ val entry1 = mock(Entry::class.java)
+ val entry2 = mock(Entry::class.java)
+
+ cache.put("iface", 2, entry1)
+ cache.put("iface", 2, entry2) // Put with the same key
+
+ assertEquals(entry2, cache.get("iface", 2))
+ }
+
+ @Test
+ fun testPut_removeLru() {
+ // Assumes max size is 2. Verify eldest entry get removed.
+ val entry1 = mock(Entry::class.java)
+ val entry2 = mock(Entry::class.java)
+ val entry3 = mock(Entry::class.java)
+
+ cache.put("iface1", 2, entry1)
+ cache.put("iface2", 4, entry2)
+ cache.put("iface3", 8, entry3)
+
+ assertNull(cache.get("iface1", 2))
+ assertEquals(entry2, cache.get("iface2", 4))
+ assertEquals(entry3, cache.get("iface3", 8))
+ }
+
+ @Test
+ fun testGetOrCompute_cacheHit() {
+ val entry1 = mock(Entry::class.java)
+
+ cache.put("iface1", 2, entry1)
+
+ // Set clock to before expiry.
+ doReturn(500L).`when`(clock).millis()
+
+ // Now call getOrCompute
+ val result = cache.getOrCompute("iface1", 2) {
+ fail("Supplier should not be called")
+ }
+
+ // Assertions
+ assertEquals(entry1, result) // Should get the cached entry.
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ @Test
+ fun testGetOrCompute_cacheMiss() {
+ val entry1 = mock(Entry::class.java)
+
+ cache.put("iface1", 2, entry1)
+
+ // Set clock to after expiry.
+ doReturn(1500L).`when`(clock).millis()
+
+ // Mock the supplier to return our network stats entry.
+ val supplier = mock(Supplier::class.java) as Supplier<Entry>
+ doReturn(entry1).`when`(supplier).get()
+
+ // Now call getOrCompute.
+ val result = cache.getOrCompute("iface1", 2, supplier)
+
+ // Assertions.
+ assertEquals(entry1, result) // Should get the cached entry.
+ verify(supplier).get()
+ }
+
+ @Test
+ fun testClear() {
+ cache.put("iface", 2, entry)
+ cache.clear()
+ assertNull(cache.get("iface", 2))
+ }
+}
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index 616da81..57a157d 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/tests/unit/vpn-jarjar-rules.txt b/tests/unit/vpn-jarjar-rules.txt
deleted file mode 100644
index 1a6bddc..0000000
--- a/tests/unit/vpn-jarjar-rules.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-# Only keep classes imported by ConnectivityServiceTest
-keep com.android.server.connectivity.Vpn
-keep com.android.server.connectivity.VpnProfileStore
-keep com.android.server.net.LockdownVpnTracker
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
index 6a5ea4b..ebbb9af 100644
--- a/thread/TEST_MAPPING
+++ b/thread/TEST_MAPPING
@@ -5,11 +5,11 @@
},
{
"name": "ThreadNetworkUnitTests"
- }
- ],
- "postsubmit": [
+ },
{
"name": "ThreadNetworkIntegrationTests"
}
+ ],
+ "postsubmit": [
]
}
diff --git a/thread/apex/Android.bp b/thread/apex/Android.bp
index 28854f2..edf000a 100644
--- a/thread/apex/Android.bp
+++ b/thread/apex/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_thread_network",
default_applicable_licenses: ["Android-Apache-2.0"],
}
diff --git a/thread/apex/ot-daemon.34rc b/thread/apex/ot-daemon.34rc
index 1eb1294..86f6b69 100644
--- a/thread/apex/ot-daemon.34rc
+++ b/thread/apex/ot-daemon.34rc
@@ -21,4 +21,5 @@
user thread_network
group thread_network inet system
seclabel u:r:ot_daemon:s0
+ socket ot-daemon/thread-wpan.sock stream 0660 thread_network thread_network
override
diff --git a/thread/demoapp/Android.bp b/thread/demoapp/Android.bp
index da7a5f8..117b4f9 100644
--- a/thread/demoapp/Android.bp
+++ b/thread/demoapp/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_fwk_thread_network",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -33,7 +34,19 @@
libs: [
"framework-connectivity-t",
],
+ required: [
+ "privapp-permissions-com.android.threadnetwork.demoapp",
+ ],
+ system_ext_specific: true,
certificate: "platform",
privileged: true,
platform_apis: true,
}
+
+prebuilt_etc {
+ name: "privapp-permissions-com.android.threadnetwork.demoapp",
+ src: "privapp-permissions-com.android.threadnetwork.demoapp.xml",
+ sub_dir: "permissions",
+ filename_from_src: true,
+ system_ext_specific: true,
+}
diff --git a/thread/demoapp/privapp-permissions-com.android.threadnetwork.demoapp.xml b/thread/demoapp/privapp-permissions-com.android.threadnetwork.demoapp.xml
new file mode 100644
index 0000000..1995e60
--- /dev/null
+++ b/thread/demoapp/privapp-permissions-com.android.threadnetwork.demoapp.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- The privileged permissions needed by the com.android.threadnetwork.demoapp app. -->
+<permissions>
+ <privapp-permissions package="com.android.threadnetwork.demoapp">
+ <permission name="android.permission.THREAD_NETWORK_PRIVILEGED" />
+ </privapp-permissions>
+</permissions>
diff --git a/thread/framework/Android.bp b/thread/framework/Android.bp
index cc598d8..f8fe422 100644
--- a/thread/framework/Android.bp
+++ b/thread/framework/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_thread_network",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -29,3 +30,14 @@
"//packages/modules/Connectivity:__subpackages__",
],
}
+
+filegroup {
+ name: "framework-thread-ot-daemon-shared-aidl-sources",
+ srcs: [
+ "java/android/net/thread/ChannelMaxPower.aidl",
+ ],
+ path: "java",
+ visibility: [
+ "//external/ot-br-posix:__subpackages__",
+ ],
+}
diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
index b74a15a..22457f5 100644
--- a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
@@ -18,7 +18,7 @@
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkState;
-import static com.android.net.module.util.HexDump.dumpHexString;
+import static com.android.net.module.util.HexDump.toHexString;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
@@ -74,42 +74,61 @@
public final class ActiveOperationalDataset implements Parcelable {
/** The maximum length of the Active Operational Dataset TLV array in bytes. */
public static final int LENGTH_MAX_DATASET_TLVS = 254;
+
/** The length of Extended PAN ID in bytes. */
public static final int LENGTH_EXTENDED_PAN_ID = 8;
+
/** The minimum length of Network Name as UTF-8 bytes. */
public static final int LENGTH_MIN_NETWORK_NAME_BYTES = 1;
+
/** The maximum length of Network Name as UTF-8 bytes. */
public static final int LENGTH_MAX_NETWORK_NAME_BYTES = 16;
+
/** The length of Network Key in bytes. */
public static final int LENGTH_NETWORK_KEY = 16;
+
/** The length of Mesh-Local Prefix in bits. */
public static final int LENGTH_MESH_LOCAL_PREFIX_BITS = 64;
+
/** The length of PSKc in bytes. */
public static final int LENGTH_PSKC = 16;
+
/** The 2.4 GHz channel page. */
public static final int CHANNEL_PAGE_24_GHZ = 0;
+
/** The minimum 2.4GHz channel. */
public static final int CHANNEL_MIN_24_GHZ = 11;
+
/** The maximum 2.4GHz channel. */
public static final int CHANNEL_MAX_24_GHZ = 26;
+
/** @hide */
@VisibleForTesting public static final int TYPE_CHANNEL = 0;
+
/** @hide */
@VisibleForTesting public static final int TYPE_PAN_ID = 1;
+
/** @hide */
@VisibleForTesting public static final int TYPE_EXTENDED_PAN_ID = 2;
+
/** @hide */
@VisibleForTesting public static final int TYPE_NETWORK_NAME = 3;
+
/** @hide */
@VisibleForTesting public static final int TYPE_PSKC = 4;
+
/** @hide */
@VisibleForTesting public static final int TYPE_NETWORK_KEY = 5;
+
/** @hide */
@VisibleForTesting public static final int TYPE_MESH_LOCAL_PREFIX = 7;
+
/** @hide */
@VisibleForTesting public static final int TYPE_SECURITY_POLICY = 12;
+
/** @hide */
@VisibleForTesting public static final int TYPE_ACTIVE_TIMESTAMP = 14;
+
/** @hide */
@VisibleForTesting public static final int TYPE_CHANNEL_MASK = 53;
@@ -591,7 +610,7 @@
sb.append("{networkName=")
.append(getNetworkName())
.append(", extendedPanId=")
- .append(dumpHexString(getExtendedPanId()))
+ .append(toHexString(getExtendedPanId()))
.append(", panId=")
.append(getPanId())
.append(", channel=")
@@ -975,8 +994,10 @@
public static final class SecurityPolicy {
/** The default Rotation Time in hours. */
public static final int DEFAULT_ROTATION_TIME_HOURS = 672;
+
/** The minimum length of Security Policy flags in bytes. */
public static final int LENGTH_MIN_SECURITY_POLICY_FLAGS = 1;
+
/** The length of Rotation Time TLV value in bytes. */
private static final int LENGTH_SECURITY_POLICY_ROTATION_TIME = 2;
@@ -1088,7 +1109,7 @@
sb.append("{rotation=")
.append(mRotationTimeHours)
.append(", flags=")
- .append(dumpHexString(mFlags))
+ .append(toHexString(mFlags))
.append("}");
return sb.toString();
}
diff --git a/thread/framework/java/android/net/thread/ChannelMaxPower.aidl b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
new file mode 100644
index 0000000..bcda8a8
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ChannelMaxPower.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+ /**
+ * Mapping from a channel to its max power.
+ *
+ * {@hide}
+ */
+parcelable ChannelMaxPower {
+ int channel; // The Thread radio channel.
+ int maxPower; // The max power in the unit of 0.01dBm. Passing INT16_MAX(32767) will
+ // disable the channel.
+}
diff --git a/thread/framework/java/android/net/thread/IStateCallback.aidl b/thread/framework/java/android/net/thread/IStateCallback.aidl
index d7cbda9..9d0a571 100644
--- a/thread/framework/java/android/net/thread/IStateCallback.aidl
+++ b/thread/framework/java/android/net/thread/IStateCallback.aidl
@@ -22,4 +22,5 @@
oneway interface IStateCallback {
void onDeviceRoleChanged(int deviceRole);
void onPartitionIdChanged(long partitionId);
+ void onThreadEnableStateChanged(int enabledState);
}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index a9da8d6..c5ca557 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -17,6 +17,7 @@
package android.net.thread;
import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.ChannelMaxPower;
import android.net.thread.IActiveOperationalDatasetReceiver;
import android.net.thread.IOperationalDatasetCallback;
import android.net.thread.IOperationReceiver;
@@ -39,7 +40,10 @@
void leave(in IOperationReceiver receiver);
void setTestNetworkAsUpstream(in String testNetworkInterfaceName, in IOperationReceiver receiver);
+ void setChannelMaxPowers(in ChannelMaxPower[] channelMaxPowers, in IOperationReceiver receiver);
int getThreadVersion();
void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver);
+
+ void setEnabled(boolean enabled, in IOperationReceiver receiver);
}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index b5699a9..8d6b40a 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -25,10 +25,12 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.annotation.Size;
import android.annotation.SystemApi;
import android.os.Binder;
import android.os.OutcomeReceiver;
import android.os.RemoteException;
+import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -68,6 +70,15 @@
/** The device is a Thread Leader. */
public static final int DEVICE_ROLE_LEADER = 4;
+ /** The Thread radio is disabled. */
+ public static final int STATE_DISABLED = 0;
+
+ /** The Thread radio is enabled. */
+ public static final int STATE_ENABLED = 1;
+
+ /** The Thread radio is being disabled. */
+ public static final int STATE_DISABLING = 2;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
@@ -79,9 +90,22 @@
})
public @interface DeviceRole {}
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = {"STATE_"},
+ value = {STATE_DISABLED, STATE_ENABLED, STATE_DISABLING})
+ public @interface EnabledState {}
+
/** Thread standard version 1.3. */
public static final int THREAD_VERSION_1_3 = 4;
+ /** Minimum value of max power in unit of 0.01dBm. @hide */
+ private static final int POWER_LIMITATION_MIN = -32768;
+
+ /** Maximum value of max power in unit of 0.01dBm. @hide */
+ private static final int POWER_LIMITATION_MAX = 32767;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({THREAD_VERSION_1_3})
@@ -106,6 +130,40 @@
mControllerService = controllerService;
}
+ /**
+ * Enables/Disables the radio of this ThreadNetworkController. The requested enabled state will
+ * be persistent and survives device reboots.
+ *
+ * <p>When Thread is in {@code STATE_DISABLED}, {@link ThreadNetworkController} APIs which
+ * require the Thread radio will fail with error code {@link
+ * ThreadNetworkException#ERROR_THREAD_DISABLED}. When Thread is in {@code STATE_DISABLING},
+ * {@link ThreadNetworkController} APIs that return a {@link ThreadNetworkException} will fail
+ * with error code {@link ThreadNetworkException#ERROR_BUSY}.
+ *
+ * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. It indicates
+ * the operation has completed. But there maybe subsequent calls to update the enabled state,
+ * callers of this method should use {@link #registerStateCallback} to subscribe to the Thread
+ * enabled state changes.
+ *
+ * <p>On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a
+ * specific error in {@link ThreadNetworkException#ERROR_}.
+ *
+ * @param enabled {@code true} for enabling Thread
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive result of this operation
+ */
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void setEnabled(
+ boolean enabled,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ try {
+ mControllerService.setEnabled(enabled, new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
/** Returns the Thread version this device is operating on. */
@ThreadVersion
public int getThreadVersion() {
@@ -170,6 +228,16 @@
* @param partitionId the new Thread partition ID
*/
default void onPartitionIdChanged(long partitionId) {}
+
+ /**
+ * The Thread enabled state has changed.
+ *
+ * <p>The Thread enabled state can be set with {@link setEnabled}, it may also be updated by
+ * airplane mode or admin control.
+ *
+ * @param enabledState the new Thread enabled state
+ */
+ default void onThreadEnableStateChanged(@EnabledState int enabledState) {}
}
private static final class StateCallbackProxy extends IStateCallback.Stub {
@@ -200,6 +268,16 @@
Binder.restoreCallingIdentity(identity);
}
}
+
+ @Override
+ public void onThreadEnableStateChanged(@EnabledState int enabled) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onThreadEnableStateChanged(enabled));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
}
/**
@@ -510,7 +588,8 @@
* @hide
*/
@VisibleForTesting
- @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ @RequiresPermission(
+ allOf = {"android.permission.THREAD_NETWORK_PRIVILEGED", permission.NETWORK_SETTINGS})
public void setTestNetworkAsUpstream(
@Nullable String testNetworkInterfaceName,
@NonNull @CallbackExecutor Executor executor,
@@ -525,6 +604,98 @@
}
}
+ /**
+ * Sets max power of each channel.
+ *
+ * <p>If not set, the default max power is set by the Thread HAL service or the Thread radio
+ * chip firmware.
+ *
+ * <p>On success, the Pending Dataset is successfully registered and persisted on the Leader and
+ * {@link OutcomeReceiver#onResult} of {@code receiver} will be called; When failed, {@link
+ * OutcomeReceiver#onError} will be called with a specific error:
+ *
+ * <ul>
+ * <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_OPERATION} the operation is no
+ * supported by the platform.
+ * </ul>
+ *
+ * @param channelMaxPowers SparseIntArray (key: channel, value: max power) consists of channel
+ * and corresponding max power. Valid channel values should be between {@link
+ * ActiveOperationalDataset#CHANNEL_MIN_24_GHZ} and {@link
+ * ActiveOperationalDataset#CHANNEL_MAX_24_GHZ}. The unit of the max power is 0.01dBm. Max
+ * power values should be between INT16_MIN (-32768) and INT16_MAX (32767). If the max power
+ * is set to INT16_MAX, the corresponding channel is not supported.
+ * @param executor the executor to execute {@code receiver}.
+ * @param receiver the receiver to receive the result of this operation.
+ * @throws IllegalArgumentException if the size of {@code channelMaxPowers} is smaller than 1,
+ * or invalid channel or max power is configured.
+ * @hide
+ */
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public final void setChannelMaxPowers(
+ @NonNull @Size(min = 1) SparseIntArray channelMaxPowers,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ requireNonNull(channelMaxPowers, "channelMaxPowers cannot be null");
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+
+ if (channelMaxPowers.size() < 1) {
+ throw new IllegalArgumentException("channelMaxPowers cannot be empty");
+ }
+
+ for (int i = 0; i < channelMaxPowers.size(); i++) {
+ int channel = channelMaxPowers.keyAt(i);
+ int maxPower = channelMaxPowers.get(channel);
+
+ if ((channel < ActiveOperationalDataset.CHANNEL_MIN_24_GHZ)
+ || (channel > ActiveOperationalDataset.CHANNEL_MAX_24_GHZ)) {
+ throw new IllegalArgumentException(
+ "Channel "
+ + channel
+ + " exceeds allowed range ["
+ + ActiveOperationalDataset.CHANNEL_MIN_24_GHZ
+ + ", "
+ + ActiveOperationalDataset.CHANNEL_MAX_24_GHZ
+ + "]");
+ }
+
+ if ((maxPower < POWER_LIMITATION_MIN) || (maxPower > POWER_LIMITATION_MAX)) {
+ throw new IllegalArgumentException(
+ "Channel power ({channel: "
+ + channel
+ + ", maxPower: "
+ + maxPower
+ + "}) exceeds allowed range ["
+ + POWER_LIMITATION_MIN
+ + ", "
+ + POWER_LIMITATION_MAX
+ + "]");
+ }
+ }
+
+ try {
+ mControllerService.setChannelMaxPowers(
+ toChannelMaxPowerArray(channelMaxPowers),
+ new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private static ChannelMaxPower[] toChannelMaxPowerArray(
+ @NonNull SparseIntArray channelMaxPowers) {
+ final ChannelMaxPower[] powerArray = new ChannelMaxPower[channelMaxPowers.size()];
+
+ for (int i = 0; i < channelMaxPowers.size(); i++) {
+ powerArray[i] = new ChannelMaxPower();
+ powerArray[i].channel = channelMaxPowers.keyAt(i);
+ powerArray[i].maxPower = channelMaxPowers.get(powerArray[i].channel);
+ }
+
+ return powerArray;
+ }
+
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..f699c30 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkException.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -47,6 +47,8 @@
ERROR_REJECTED_BY_PEER,
ERROR_RESPONSE_BAD_FORMAT,
ERROR_RESOURCE_EXHAUSTED,
+ ERROR_UNKNOWN,
+ ERROR_THREAD_DISABLED,
})
public @interface ErrorCode {}
@@ -87,8 +89,9 @@
/**
* The operation failed because required preconditions were not satisfied. For example, trying
- * to schedule a network migration when this device is not attached will receive this error. The
- * caller should not retry the same operation before the precondition is satisfied.
+ * to schedule a network migration when this device is not attached will receive this error or
+ * enable Thread while User Resitration has disabled it. The caller should not retry the same
+ * operation before the precondition is satisfied.
*/
public static final int ERROR_FAILED_PRECONDITION = 6;
@@ -122,11 +125,51 @@
*/
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;
+
+ /**
+ * The operation failed because the Thread radio is disabled by {@link
+ * ThreadNetworkController#setEnabled}, airplane mode or device admin. The caller should retry
+ * only after Thread is enabled.
+ */
+ public static final int ERROR_THREAD_DISABLED = 12;
+
+ /**
+ * The operation failed because it is not supported by the platform. For example, some platforms
+ * may not support setting the target power of each channel. The caller should not retry and may
+ * return an error to the user.
+ *
+ * @hide
+ */
+ public static final int ERROR_UNSUPPORTED_OPERATION = 13;
+
+ private static final int ERROR_MIN = ERROR_INTERNAL_ERROR;
+ private static final int ERROR_MAX = ERROR_UNSUPPORTED_OPERATION;
+
private final int mErrorCode;
- /** Creates a new {@link ThreadNetworkException} object with given error code and message. */
- public ThreadNetworkException(@ErrorCode int errorCode, @NonNull String errorMessage) {
- super(requireNonNull(errorMessage, "errorMessage cannot be null"));
+ /**
+ * Creates a new {@link ThreadNetworkException} object with given error code and message.
+ *
+ * @throws IllegalArgumentException if {@code errorCode} is not a value in {@link #ERROR_}
+ * @throws NullPointerException if {@code message} is {@code null}
+ */
+ public ThreadNetworkException(@ErrorCode int errorCode, @NonNull String message) {
+ super(requireNonNull(message, "message cannot be null"));
+ if (errorCode < ERROR_MIN || errorCode > ERROR_MAX) {
+ throw new IllegalArgumentException(
+ "errorCode cannot be "
+ + errorCode
+ + " (allowedRange = ["
+ + ERROR_MIN
+ + ", "
+ + ERROR_MAX
+ + "])");
+ }
this.mErrorCode = errorCode;
}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
index 28012a7..150b759 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkManager.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -79,6 +79,17 @@
public static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
+ /**
+ * This user restriction specifies if Thread network is disallowed on the device. If Thread
+ * network is disallowed it cannot be turned on via Settings.
+ *
+ * <p>this is a mirror of {@link UserManager#DISALLOW_THREAD_NETWORK} which is not available on
+ * Android U devices.
+ *
+ * @hide
+ */
+ public static final String DISALLOW_THREAD_NETWORK = "no_thread_network";
+
@NonNull private final Context mContext;
@NonNull private final List<ThreadNetworkController> mUnmodifiableControllerServices;
diff --git a/thread/scripts/make-pretty.sh b/thread/scripts/make-pretty.sh
index e4bd459..c176bfa 100755
--- a/thread/scripts/make-pretty.sh
+++ b/thread/scripts/make-pretty.sh
@@ -3,5 +3,7 @@
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
GOOGLE_JAVA_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/tools/common/google-java-format/google-java-format
+ANDROID_BP_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/build-tools/linux-x86/bin/bpfmt
$GOOGLE_JAVA_FORMAT --aosp -i $(find $SCRIPT_DIR/../ -name "*.java")
+$ANDROID_BP_FORMAT -w $(find $SCRIPT_DIR/../ -name "*.bp")
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
index 69295cc..a82a499 100644
--- a/thread/service/Android.bp
+++ b/thread/service/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_thread_network",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -44,12 +45,12 @@
"modules-utils-shell-command-handler",
"net-utils-device-common",
"net-utils-device-common-netlink",
+ // The required dependency net-utils-device-common-struct-base is in the classpath via
+ // framework-connectivity
+ "net-utils-device-common-struct",
"ot-daemon-aidl-java",
],
apex_available: ["com.android.tethering"],
- optimize: {
- proguard_flags_files: ["proguard.flags"],
- },
}
cc_library_shared {
diff --git a/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
new file mode 100644
index 0000000..43ff336
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
@@ -0,0 +1,100 @@
+/*
+ * 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 android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
+
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IActiveOperationalDatasetReceiver;
+import android.net.thread.ThreadNetworkException;
+import android.os.RemoteException;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A {@link IActiveOperationalDatasetReceiver} wrapper which makes it easier to invoke the
+ * callbacks.
+ */
+final class ActiveOperationalDatasetReceiverWrapper {
+ private final IActiveOperationalDatasetReceiver mReceiver;
+
+ private static final Object sPendingReceiversLock = new Object();
+
+ @GuardedBy("sPendingReceiversLock")
+ private static final Set<ActiveOperationalDatasetReceiverWrapper> sPendingReceivers =
+ new HashSet<>();
+
+ public ActiveOperationalDatasetReceiverWrapper(IActiveOperationalDatasetReceiver receiver) {
+ this.mReceiver = receiver;
+
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.add(this);
+ }
+ }
+
+ public static void onOtDaemonDied() {
+ synchronized (sPendingReceiversLock) {
+ for (ActiveOperationalDatasetReceiverWrapper receiver : sPendingReceivers) {
+ try {
+ receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+ sPendingReceivers.clear();
+ }
+ }
+
+ public void onSuccess(ActiveOperationalDataset dataset) {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onSuccess(dataset);
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+
+ public void onError(Throwable e) {
+ if (e instanceof ThreadNetworkException) {
+ ThreadNetworkException threadException = (ThreadNetworkException) e;
+ onError(threadException.getErrorCode(), threadException.getMessage());
+ } else if (e instanceof RemoteException) {
+ onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ } else {
+ throw new AssertionError(e);
+ }
+ }
+
+ public void onError(int errorCode, String errorMessage) {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onError(errorCode, errorMessage);
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/NsdPublisher.java b/thread/service/java/com/android/server/thread/NsdPublisher.java
new file mode 100644
index 0000000..2c14f1d
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/NsdPublisher.java
@@ -0,0 +1,589 @@
+/*
+ * 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 android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.InetAddresses;
+import android.net.nsd.DiscoveryRequest;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.thread.openthread.DnsTxtAttribute;
+import com.android.server.thread.openthread.INsdDiscoverServiceCallback;
+import com.android.server.thread.openthread.INsdPublisher;
+import com.android.server.thread.openthread.INsdResolveServiceCallback;
+import com.android.server.thread.openthread.INsdStatusReceiver;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Implementation of {@link INsdPublisher}.
+ *
+ * <p>This class provides API for service registration and discovery over mDNS. This class is a
+ * proxy between ot-daemon and NsdManager.
+ *
+ * <p>All the data members of this class MUST be accessed in the {@code mHandler}'s Thread except
+ * {@code mHandler} itself.
+ */
+public final class NsdPublisher extends INsdPublisher.Stub {
+ // TODO: b/321883491 - specify network for mDNS operations
+ private static final String TAG = NsdPublisher.class.getSimpleName();
+ private final NsdManager mNsdManager;
+ private final Handler mHandler;
+ private final Executor mExecutor;
+ private final SparseArray<RegistrationListener> mRegistrationListeners = new SparseArray<>(0);
+ private final SparseArray<DiscoveryListener> mDiscoveryListeners = new SparseArray<>(0);
+ private final SparseArray<ServiceInfoListener> mServiceInfoListeners = new SparseArray<>(0);
+
+ @VisibleForTesting
+ public NsdPublisher(NsdManager nsdManager, Handler handler) {
+ mNsdManager = nsdManager;
+ mHandler = handler;
+ mExecutor = runnable -> mHandler.post(runnable);
+ }
+
+ public static NsdPublisher newInstance(Context context, Handler handler) {
+ return new NsdPublisher(context.getSystemService(NsdManager.class), handler);
+ }
+
+ @Override
+ public void registerService(
+ String hostname,
+ String name,
+ String type,
+ List<String> subTypeList,
+ int port,
+ List<DnsTxtAttribute> txt,
+ INsdStatusReceiver receiver,
+ int listenerId) {
+ NsdServiceInfo serviceInfo =
+ buildServiceInfoForService(hostname, name, type, subTypeList, port, txt);
+ mHandler.post(() -> registerInternal(serviceInfo, receiver, listenerId, "service"));
+ }
+
+ private static NsdServiceInfo buildServiceInfoForService(
+ String hostname,
+ String name,
+ String type,
+ List<String> subTypeList,
+ int port,
+ List<DnsTxtAttribute> txt) {
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+
+ serviceInfo.setServiceName(name);
+ if (!TextUtils.isEmpty(hostname)) {
+ serviceInfo.setHostname(hostname);
+ }
+ serviceInfo.setServiceType(type);
+ serviceInfo.setPort(port);
+ serviceInfo.setSubtypes(new HashSet<>(subTypeList));
+ for (DnsTxtAttribute attribute : txt) {
+ serviceInfo.setAttribute(attribute.name, attribute.value);
+ }
+
+ return serviceInfo;
+ }
+
+ @Override
+ public void registerHost(
+ String name, List<String> addresses, INsdStatusReceiver receiver, int listenerId) {
+ NsdServiceInfo serviceInfo = buildServiceInfoForHost(name, addresses);
+ mHandler.post(() -> registerInternal(serviceInfo, receiver, listenerId, "host"));
+ }
+
+ private static NsdServiceInfo buildServiceInfoForHost(
+ String name, List<String> addressStrings) {
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+
+ serviceInfo.setHostname(name);
+ ArrayList<InetAddress> addresses = new ArrayList<>(addressStrings.size());
+ for (String addressString : addressStrings) {
+ addresses.add(InetAddresses.parseNumericAddress(addressString));
+ }
+ serviceInfo.setHostAddresses(addresses);
+
+ return serviceInfo;
+ }
+
+ private void registerInternal(
+ NsdServiceInfo serviceInfo,
+ INsdStatusReceiver receiver,
+ int listenerId,
+ String registrationType) {
+ checkOnHandlerThread();
+ Log.i(
+ TAG,
+ "Registering "
+ + registrationType
+ + ". Listener ID: "
+ + listenerId
+ + ", serviceInfo: "
+ + serviceInfo);
+ RegistrationListener listener = new RegistrationListener(serviceInfo, listenerId, receiver);
+ mRegistrationListeners.append(listenerId, listener);
+ try {
+ mNsdManager.registerService(serviceInfo, PROTOCOL_DNS_SD, mExecutor, listener);
+ } catch (IllegalArgumentException e) {
+ Log.i(TAG, "Failed to register service. serviceInfo: " + serviceInfo, e);
+ listener.onRegistrationFailed(serviceInfo, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+ }
+
+ public void unregister(INsdStatusReceiver receiver, int listenerId) {
+ mHandler.post(() -> unregisterInternal(receiver, listenerId));
+ }
+
+ public void unregisterInternal(INsdStatusReceiver receiver, int listenerId) {
+ checkOnHandlerThread();
+ RegistrationListener registrationListener = mRegistrationListeners.get(listenerId);
+ if (registrationListener == null) {
+ Log.w(
+ TAG,
+ "Failed to unregister service."
+ + " Listener ID: "
+ + listenerId
+ + " The registrationListener is empty.");
+
+ return;
+ }
+ Log.i(
+ TAG,
+ "Unregistering service."
+ + " Listener ID: "
+ + listenerId
+ + " serviceInfo: "
+ + registrationListener.mServiceInfo);
+ registrationListener.addUnregistrationReceiver(receiver);
+ mNsdManager.unregisterService(registrationListener);
+ }
+
+ @Override
+ public void discoverService(String type, INsdDiscoverServiceCallback callback, int listenerId) {
+ mHandler.post(() -> discoverServiceInternal(type, callback, listenerId));
+ }
+
+ private void discoverServiceInternal(
+ String type, INsdDiscoverServiceCallback callback, int listenerId) {
+ checkOnHandlerThread();
+ Log.i(
+ TAG,
+ "Discovering services."
+ + " Listener ID: "
+ + listenerId
+ + ", service type: "
+ + type);
+
+ DiscoveryListener listener = new DiscoveryListener(listenerId, type, callback);
+ mDiscoveryListeners.append(listenerId, listener);
+ DiscoveryRequest discoveryRequest =
+ new DiscoveryRequest.Builder(type).setNetwork(null).build();
+ mNsdManager.discoverServices(discoveryRequest, mExecutor, listener);
+ }
+
+ @Override
+ public void stopServiceDiscovery(int listenerId) {
+ mHandler.post(() -> stopServiceDiscoveryInternal(listenerId));
+ }
+
+ private void stopServiceDiscoveryInternal(int listenerId) {
+ checkOnHandlerThread();
+
+ DiscoveryListener listener = mDiscoveryListeners.get(listenerId);
+ if (listener == null) {
+ Log.w(
+ TAG,
+ "Failed to stop service discovery. Listener ID "
+ + listenerId
+ + ". The listener is null.");
+ return;
+ }
+
+ Log.i(TAG, "Stopping service discovery. Listener: " + listener);
+ mNsdManager.stopServiceDiscovery(listener);
+ }
+
+ @Override
+ public void resolveService(
+ String name, String type, INsdResolveServiceCallback callback, int listenerId) {
+ mHandler.post(() -> resolveServiceInternal(name, type, callback, listenerId));
+ }
+
+ private void resolveServiceInternal(
+ String name, String type, INsdResolveServiceCallback callback, int listenerId) {
+ checkOnHandlerThread();
+
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+ serviceInfo.setServiceName(name);
+ serviceInfo.setServiceType(type);
+ serviceInfo.setNetwork(null);
+ Log.i(
+ TAG,
+ "Resolving service."
+ + " Listener ID: "
+ + listenerId
+ + ", service name: "
+ + name
+ + ", service type: "
+ + type);
+
+ ServiceInfoListener listener = new ServiceInfoListener(serviceInfo, listenerId, callback);
+ mServiceInfoListeners.append(listenerId, listener);
+ mNsdManager.registerServiceInfoCallback(serviceInfo, mExecutor, listener);
+ }
+
+ @Override
+ public void stopServiceResolution(int listenerId) {
+ mHandler.post(() -> stopServiceResolutionInternal(listenerId));
+ }
+
+ private void stopServiceResolutionInternal(int listenerId) {
+ checkOnHandlerThread();
+
+ ServiceInfoListener listener = mServiceInfoListeners.get(listenerId);
+ if (listener == null) {
+ Log.w(
+ TAG,
+ "Failed to stop service resolution. Listener ID: "
+ + listenerId
+ + ". The listener is null.");
+ return;
+ }
+
+ Log.i(TAG, "Stopping service resolution. Listener: " + listener);
+
+ try {
+ mNsdManager.unregisterServiceInfoCallback(listener);
+ } catch (IllegalArgumentException e) {
+ Log.w(
+ TAG,
+ "Failed to stop the service resolution because it's already stopped. Listener: "
+ + listener);
+ }
+ }
+
+ private void checkOnHandlerThread() {
+ if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+ throw new IllegalStateException(
+ "Not running on handler Thread: " + Thread.currentThread().getName());
+ }
+ }
+
+ @Override
+ public void reset() {
+ mHandler.post(this::resetInternal);
+ }
+
+ private void resetInternal() {
+ checkOnHandlerThread();
+ for (int i = 0; i < mRegistrationListeners.size(); ++i) {
+ try {
+ mNsdManager.unregisterService(mRegistrationListeners.valueAt(i));
+ } catch (IllegalArgumentException e) {
+ Log.i(
+ TAG,
+ "Failed to unregister."
+ + " Listener ID: "
+ + mRegistrationListeners.keyAt(i)
+ + " serviceInfo: "
+ + mRegistrationListeners.valueAt(i).mServiceInfo,
+ e);
+ }
+ }
+ mRegistrationListeners.clear();
+ }
+
+ /** On ot-daemon died, reset. */
+ public void onOtDaemonDied() {
+ reset();
+ }
+
+ private final class RegistrationListener implements NsdManager.RegistrationListener {
+ private final NsdServiceInfo mServiceInfo;
+ private final int mListenerId;
+ private final INsdStatusReceiver mRegistrationReceiver;
+ private final List<INsdStatusReceiver> mUnregistrationReceivers;
+
+ RegistrationListener(
+ @NonNull NsdServiceInfo serviceInfo,
+ int listenerId,
+ @NonNull INsdStatusReceiver registrationReceiver) {
+ mServiceInfo = serviceInfo;
+ mListenerId = listenerId;
+ mRegistrationReceiver = registrationReceiver;
+ mUnregistrationReceivers = new ArrayList<>();
+ }
+
+ void addUnregistrationReceiver(@NonNull INsdStatusReceiver unregistrationReceiver) {
+ mUnregistrationReceivers.add(unregistrationReceiver);
+ }
+
+ @Override
+ public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ checkOnHandlerThread();
+ mRegistrationListeners.remove(mListenerId);
+ Log.i(
+ TAG,
+ "Failed to register listener ID: "
+ + mListenerId
+ + " error code: "
+ + errorCode
+ + " serviceInfo: "
+ + serviceInfo);
+ try {
+ mRegistrationReceiver.onError(errorCode);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+
+ @Override
+ public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ checkOnHandlerThread();
+ for (INsdStatusReceiver receiver : mUnregistrationReceivers) {
+ Log.i(
+ TAG,
+ "Failed to unregister."
+ + "Listener ID: "
+ + mListenerId
+ + ", error code: "
+ + errorCode
+ + ", serviceInfo: "
+ + serviceInfo);
+ try {
+ receiver.onError(errorCode);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+
+ @Override
+ public void onServiceRegistered(NsdServiceInfo serviceInfo) {
+ checkOnHandlerThread();
+ Log.i(
+ TAG,
+ "Registered successfully. "
+ + "Listener ID: "
+ + mListenerId
+ + ", serviceInfo: "
+ + serviceInfo);
+ try {
+ mRegistrationReceiver.onSuccess();
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+
+ @Override
+ public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
+ checkOnHandlerThread();
+ for (INsdStatusReceiver receiver : mUnregistrationReceivers) {
+ Log.i(
+ TAG,
+ "Unregistered successfully. "
+ + "Listener ID: "
+ + mListenerId
+ + ", serviceInfo: "
+ + serviceInfo);
+ try {
+ receiver.onSuccess();
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ mRegistrationListeners.remove(mListenerId);
+ }
+ }
+
+ private final class DiscoveryListener implements NsdManager.DiscoveryListener {
+ private final int mListenerId;
+ private final String mType;
+ private final INsdDiscoverServiceCallback mDiscoverServiceCallback;
+
+ DiscoveryListener(
+ int listenerId,
+ @NonNull String type,
+ @NonNull INsdDiscoverServiceCallback discoverServiceCallback) {
+ mListenerId = listenerId;
+ mType = type;
+ mDiscoverServiceCallback = discoverServiceCallback;
+ }
+
+ @Override
+ public void onStartDiscoveryFailed(String serviceType, int errorCode) {
+ Log.e(
+ TAG,
+ "Failed to start service discovery."
+ + " Error code: "
+ + errorCode
+ + ", listener: "
+ + this);
+ mDiscoveryListeners.remove(mListenerId);
+ }
+
+ @Override
+ public void onStopDiscoveryFailed(String serviceType, int errorCode) {
+ Log.e(
+ TAG,
+ "Failed to stop service discovery."
+ + " Error code: "
+ + errorCode
+ + ", listener: "
+ + this);
+ mDiscoveryListeners.remove(mListenerId);
+ }
+
+ @Override
+ public void onDiscoveryStarted(String serviceType) {
+ Log.i(TAG, "Started service discovery. Listener: " + this);
+ }
+
+ @Override
+ public void onDiscoveryStopped(String serviceType) {
+ Log.i(TAG, "Stopped service discovery. Listener: " + this);
+ mDiscoveryListeners.remove(mListenerId);
+ }
+
+ @Override
+ public void onServiceFound(NsdServiceInfo serviceInfo) {
+ Log.i(TAG, "Found service: " + serviceInfo);
+ try {
+ mDiscoverServiceCallback.onServiceDiscovered(
+ serviceInfo.getServiceName(), mType, true);
+ } catch (RemoteException e) {
+ // do nothing if the client is dead
+ }
+ }
+
+ @Override
+ public void onServiceLost(NsdServiceInfo serviceInfo) {
+ Log.i(TAG, "Lost service: " + serviceInfo);
+ try {
+ mDiscoverServiceCallback.onServiceDiscovered(
+ serviceInfo.getServiceName(), mType, false);
+ } catch (RemoteException e) {
+ // do nothing if the client is dead
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ID: " + mListenerId + ", type: " + mType;
+ }
+ }
+
+ private final class ServiceInfoListener implements NsdManager.ServiceInfoCallback {
+ private final String mName;
+ private final String mType;
+ private final INsdResolveServiceCallback mResolveServiceCallback;
+ private final int mListenerId;
+
+ ServiceInfoListener(
+ @NonNull NsdServiceInfo serviceInfo,
+ int listenerId,
+ @NonNull INsdResolveServiceCallback resolveServiceCallback) {
+ mName = serviceInfo.getServiceName();
+ mType = serviceInfo.getServiceType();
+ mListenerId = listenerId;
+ mResolveServiceCallback = resolveServiceCallback;
+ }
+
+ @Override
+ public void onServiceInfoCallbackRegistrationFailed(int errorCode) {
+ Log.e(
+ TAG,
+ "Failed to register service info callback."
+ + " Listener ID: "
+ + mListenerId
+ + ", error: "
+ + errorCode
+ + ", service name: "
+ + mName
+ + ", service type: "
+ + mType);
+ }
+
+ @Override
+ public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
+ Log.i(
+ TAG,
+ "Service is resolved. "
+ + " Listener ID: "
+ + mListenerId
+ + ", serviceInfo: "
+ + serviceInfo);
+ List<String> addresses = new ArrayList<>();
+ for (InetAddress address : serviceInfo.getHostAddresses()) {
+ if (address instanceof Inet6Address) {
+ addresses.add(address.getHostAddress());
+ }
+ }
+ List<DnsTxtAttribute> txtList = new ArrayList<>();
+ for (Map.Entry<String, byte[]> entry : serviceInfo.getAttributes().entrySet()) {
+ DnsTxtAttribute attribute = new DnsTxtAttribute();
+ attribute.name = entry.getKey();
+ attribute.value = Arrays.copyOf(entry.getValue(), entry.getValue().length);
+ txtList.add(attribute);
+ }
+ // TODO: b/329018320 - Use the serviceInfo.getExpirationTime to derive TTL.
+ int ttlSeconds = 10;
+ try {
+ mResolveServiceCallback.onServiceResolved(
+ serviceInfo.getHostname(),
+ serviceInfo.getServiceName(),
+ serviceInfo.getServiceType(),
+ serviceInfo.getPort(),
+ addresses,
+ txtList,
+ ttlSeconds);
+
+ } catch (RemoteException e) {
+ // do nothing if the client is dead
+ }
+ }
+
+ @Override
+ public void onServiceLost() {}
+
+ @Override
+ public void onServiceInfoCallbackUnregistered() {
+ Log.i(TAG, "The service info callback is unregistered. Listener: " + this);
+ mServiceInfoListeners.remove(mListenerId);
+ }
+
+ @Override
+ public String toString() {
+ return "ID: " + mListenerId + ", service name: " + mName + ", service type: " + mType;
+ }
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java b/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java
index a8909bc..bad63f3 100644
--- a/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java
+++ b/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java
@@ -16,9 +16,11 @@
package com.android.server.thread;
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
import android.net.thread.IOperationReceiver;
+import android.net.thread.ThreadNetworkException;
import android.os.RemoteException;
import com.android.internal.annotations.GuardedBy;
@@ -29,6 +31,7 @@
/** A {@link IOperationReceiver} wrapper which makes it easier to invoke the callbacks. */
final class OperationReceiverWrapper {
private final IOperationReceiver mReceiver;
+ private final boolean mExpectOtDaemonDied;
private static final Object sPendingReceiversLock = new Object();
@@ -36,7 +39,19 @@
private static final Set<OperationReceiverWrapper> sPendingReceivers = new HashSet<>();
public OperationReceiverWrapper(IOperationReceiver receiver) {
- this.mReceiver = receiver;
+ this(receiver, false /* expectOtDaemonDied */);
+ }
+
+ /**
+ * Creates a new {@link OperationReceiverWrapper}.
+ *
+ * <p>If {@code expectOtDaemonDied} is {@code true}, it's expected that ot-daemon becomes dead
+ * before {@code receiver} is completed with {@code onSuccess} and {@code onError} and {@code
+ * receiver#onSuccess} will be invoked in this case.
+ */
+ public OperationReceiverWrapper(IOperationReceiver receiver, boolean expectOtDaemonDied) {
+ mReceiver = receiver;
+ mExpectOtDaemonDied = expectOtDaemonDied;
synchronized (sPendingReceiversLock) {
sPendingReceivers.add(this);
@@ -47,7 +62,11 @@
synchronized (sPendingReceiversLock) {
for (OperationReceiverWrapper receiver : sPendingReceivers) {
try {
- receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
+ if (receiver.mExpectOtDaemonDied) {
+ receiver.mReceiver.onSuccess();
+ } else {
+ receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
+ }
} catch (RemoteException e) {
// The client is dead, do nothing
}
@@ -68,6 +87,17 @@
}
}
+ public void onError(Throwable e) {
+ if (e instanceof ThreadNetworkException) {
+ ThreadNetworkException threadException = (ThreadNetworkException) e;
+ onError(threadException.getErrorCode(), threadException.getMessage());
+ } else if (e instanceof RemoteException) {
+ onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ } else {
+ throw new AssertionError(e);
+ }
+ }
+
public void onError(int errorCode, String errorMessage, Object... messageArgs) {
synchronized (sPendingReceiversLock) {
sPendingReceivers.remove(this);
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 6cd0ac3..0559499 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -14,8 +14,8 @@
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;
import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE;
import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
@@ -26,6 +26,9 @@
import static android.net.thread.ActiveOperationalDataset.MESH_LOCAL_PREFIX_FIRST_BYTE;
import static android.net.thread.ActiveOperationalDataset.SecurityPolicy.DEFAULT_ROTATION_TIME_HOURS;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
+import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
+import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
+import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
import static android.net.thread.ThreadNetworkException.ERROR_BUSY;
@@ -34,20 +37,28 @@
import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
import static android.net.thread.ThreadNetworkException.ERROR_RESOURCE_EXHAUSTED;
import static android.net.thread.ThreadNetworkException.ERROR_RESPONSE_BAD_FORMAT;
+import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
+import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_BUSY;
-import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_DETACHED;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_FAILED_PRECONDITION;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_NOT_IMPLEMENTED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_NO_BUFS;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_PARSE;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_REASSEMBLY_TIMEOUT;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_REJECTED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_RESPONSE_TIMEOUT;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_THREAD_DISABLED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_UNSUPPORTED_CHANNEL;
+import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_DISABLED;
+import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_DISABLING;
+import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_ENABLED;
import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME;
import android.Manifest.permission;
@@ -55,10 +66,13 @@
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
import android.net.ConnectivityManager;
-import android.net.IpPrefix;
-import android.net.LinkAddress;
+import android.net.InetAddresses;
import android.net.LinkProperties;
import android.net.LocalNetworkConfig;
import android.net.LocalNetworkInfo;
@@ -70,10 +84,10 @@
import android.net.NetworkProvider;
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.ChannelMaxPower;
import android.net.thread.IActiveOperationalDatasetReceiver;
import android.net.thread.IOperationReceiver;
import android.net.thread.IOperationalDatasetCallback;
@@ -83,6 +97,7 @@
import android.net.thread.PendingOperationalDataset;
import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkController.DeviceRole;
+import android.net.thread.ThreadNetworkException;
import android.net.thread.ThreadNetworkException.ErrorCode;
import android.os.Build;
import android.os.Handler;
@@ -90,30 +105,38 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
-import android.os.SystemClock;
+import android.os.UserManager;
import android.util.Log;
import android.util.SparseArray;
+import com.android.connectivity.resources.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.ServiceManagerWrapper;
+import com.android.server.connectivity.ConnectivityResources;
+import com.android.server.thread.openthread.BackboneRouterState;
import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
+import com.android.server.thread.openthread.IChannelMasksReceiver;
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.MeshcopTxtAttributes;
import com.android.server.thread.openthread.OtDaemonState;
+import libcore.util.HexEncoding;
+
import java.io.IOException;
import java.net.Inet6Address;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.function.Supplier;
+import java.util.regex.Pattern;
/**
* Implementation of the {@link ThreadNetworkController} API.
@@ -128,6 +151,19 @@
final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
private static final String TAG = "ThreadNetworkService";
+ // The model name length in utf-8 bytes
+ private static final int MAX_MODEL_NAME_UTF8_BYTES = 24;
+
+ // The max vendor name length in utf-8 bytes
+ private static final int MAX_VENDOR_NAME_UTF8_BYTES = 24;
+
+ // This regex pattern allows "XXXXXX", "XX:XX:XX" and "XX-XX-XX" OUI formats.
+ // Note that this regex allows "XX:XX-XX" as well but we don't need to be a strict checker
+ private static final String OUI_REGEX = "^([0-9A-Fa-f]{2}[:-]?){2}([0-9A-Fa-f]{2})$";
+
+ // The channel mask that indicates all channels from channel 11 to channel 24
+ private static final int CHANNEL_MASK_11_TO_24 = 0x1FFF800;
+
// Below member fields can be accessed from both the binder and handler threads
private final Context mContext;
@@ -142,12 +178,13 @@
private final ConnectivityManager mConnectivityManager;
private final TunInterfaceController mTunIfController;
private final InfraInterfaceController mInfraIfController;
- private final LinkProperties mLinkProperties = new LinkProperties();
+ private final NsdPublisher mNsdPublisher;
private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
+ private final ConnectivityResources mResources;
+ private final Supplier<String> mCountryCodeSupplier;
- // TODO(b/308310823): read supported channel from Thread dameon
- private final int mSupportedChannelMask = 0x07FFF800; // from channel 11 to 26
-
+ // This should not be directly used for calling IOtDaemon APIs because ot-daemon may die and
+ // {@code mOtDaemon} will be set to {@code null}. Instead, use {@code getOtDaemon()}
@Nullable private IOtDaemon mOtDaemon;
@Nullable private NetworkAgent mNetworkAgent;
@Nullable private NetworkAgent mTestNetworkAgent;
@@ -159,6 +196,10 @@
private UpstreamNetworkCallback mUpstreamNetworkCallback;
private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
private final HashMap<Network, String> mNetworkToInterface;
+ private final ThreadPersistentSettings mPersistentSettings;
+ private final UserManager mUserManager;
+ private boolean mUserRestricted;
+ private boolean mForceStopOtDaemonEnabled;
private BorderRouterConfigurationParcel mBorderRouterConfig;
@@ -170,7 +211,12 @@
Supplier<IOtDaemon> otDaemonSupplier,
ConnectivityManager connectivityManager,
TunInterfaceController tunIfController,
- InfraInterfaceController infraIfController) {
+ InfraInterfaceController infraIfController,
+ ThreadPersistentSettings persistentSettings,
+ NsdPublisher nsdPublisher,
+ UserManager userManager,
+ ConnectivityResources resources,
+ Supplier<String> countryCodeSupplier) {
mContext = context;
mHandler = handler;
mNetworkProvider = networkProvider;
@@ -181,54 +227,36 @@
mUpstreamNetworkRequest = newUpstreamNetworkRequest();
mNetworkToInterface = new HashMap<Network, String>();
mBorderRouterConfig = new BorderRouterConfigurationParcel();
+ mPersistentSettings = persistentSettings;
+ mNsdPublisher = nsdPublisher;
+ mUserManager = userManager;
+ mResources = resources;
+ mCountryCodeSupplier = countryCodeSupplier;
}
- public static ThreadNetworkControllerService newInstance(Context context) {
+ public static ThreadNetworkControllerService newInstance(
+ Context context,
+ ThreadPersistentSettings persistentSettings,
+ Supplier<String> countryCodeSupplier) {
HandlerThread handlerThread = new HandlerThread("ThreadHandlerThread");
handlerThread.start();
+ Handler handler = new Handler(handlerThread.getLooper());
NetworkProvider networkProvider =
new NetworkProvider(context, handlerThread.getLooper(), "ThreadNetworkProvider");
return new ThreadNetworkControllerService(
context,
- new Handler(handlerThread.getLooper()),
+ handler,
networkProvider,
() -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
context.getSystemService(ConnectivityManager.class),
new TunInterfaceController(TUN_IF_NAME),
- new InfraInterfaceController());
- }
-
- private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
- try {
- return (Inet6Address) Inet6Address.getByAddress(addressBytes);
- } catch (UnknownHostException e) {
- // This is unlikely to happen unless the Thread daemon is critically broken
- return null;
- }
- }
-
- private static InetAddress addressInfoToInetAddress(Ipv6AddressInfo addressInfo) {
- return bytesToInet6Address(addressInfo.address);
- }
-
- private static LinkAddress newLinkAddress(Ipv6AddressInfo addressInfo) {
- long deprecationTimeMillis =
- addressInfo.isPreferred
- ? LinkAddress.LIFETIME_PERMANENT
- : SystemClock.elapsedRealtime();
-
- InetAddress address = addressInfoToInetAddress(addressInfo);
-
- // flags and scope will be adjusted automatically depending on the address and
- // its lifetimes.
- return new LinkAddress(
- address,
- addressInfo.prefixLength,
- 0 /* flags */,
- 0 /* scope */,
- deprecationTimeMillis,
- LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
+ new InfraInterfaceController(),
+ persistentSettings,
+ NsdPublisher.newInstance(context, handler),
+ context.getSystemService(UserManager.class),
+ new ConnectivityResources(context),
+ countryCodeSupplier);
}
private NetworkRequest newUpstreamNetworkRequest() {
@@ -253,49 +281,31 @@
.build();
}
- @Override
- public void setTestNetworkAsUpstream(
- @Nullable String testNetworkInterfaceName, @NonNull IOperationReceiver receiver) {
- enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- 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);
+ private void maybeInitializeOtDaemon() {
+ if (!isEnabled()) {
+ return;
}
- 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
- }
- }
+ Log.i(TAG, "Starting OT daemon...");
- private void initializeOtDaemon() {
try {
getOtDaemon();
} catch (RemoteException e) {
- Log.e(TAG, "Failed to initialize ot-daemon");
+ Log.e(TAG, "Failed to initialize ot-daemon", e);
+ } catch (ThreadNetworkException e) {
+ // no ThreadNetworkException.ERROR_THREAD_DISABLED error should be thrown
+ throw new AssertionError(e);
}
}
- private IOtDaemon getOtDaemon() throws RemoteException {
+ private IOtDaemon getOtDaemon() throws RemoteException, ThreadNetworkException {
checkOnHandlerThread();
+ if (mForceStopOtDaemonEnabled) {
+ throw new ThreadNetworkException(
+ ERROR_THREAD_DISABLED, "ot-daemon is forcibly stopped");
+ }
+
if (mOtDaemon != null) {
return mOtDaemon;
}
@@ -304,40 +314,239 @@
if (otDaemon == null) {
throw new RemoteException("Internal error: failed to start OT daemon");
}
- otDaemon.initialize(mTunIfController.getTunFd());
- otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
+
+ otDaemon.initialize(
+ mTunIfController.getTunFd(),
+ isEnabled(),
+ mNsdPublisher,
+ getMeshcopTxtAttributes(mResources.get()),
+ mOtDaemonCallbackProxy,
+ mCountryCodeSupplier.get());
otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
mOtDaemon = otDaemon;
return mOtDaemon;
}
- // TODO(b/309792480): restarts the OT daemon service
+ @VisibleForTesting
+ static MeshcopTxtAttributes getMeshcopTxtAttributes(Resources resources) {
+ final String modelName = resources.getString(R.string.config_thread_model_name);
+ final String vendorName = resources.getString(R.string.config_thread_vendor_name);
+ final String vendorOui = resources.getString(R.string.config_thread_vendor_oui);
+
+ if (!modelName.isEmpty()) {
+ if (modelName.getBytes(StandardCharsets.UTF_8).length > MAX_MODEL_NAME_UTF8_BYTES) {
+ throw new IllegalStateException(
+ "Model name is longer than "
+ + MAX_MODEL_NAME_UTF8_BYTES
+ + "utf-8 bytes: "
+ + modelName);
+ }
+ }
+
+ if (!vendorName.isEmpty()) {
+ if (vendorName.getBytes(StandardCharsets.UTF_8).length > MAX_VENDOR_NAME_UTF8_BYTES) {
+ throw new IllegalStateException(
+ "Vendor name is longer than "
+ + MAX_VENDOR_NAME_UTF8_BYTES
+ + " utf-8 bytes: "
+ + vendorName);
+ }
+ }
+
+ if (!vendorOui.isEmpty() && !Pattern.compile(OUI_REGEX).matcher(vendorOui).matches()) {
+ throw new IllegalStateException("Vendor OUI is invalid: " + vendorOui);
+ }
+
+ MeshcopTxtAttributes meshcopTxts = new MeshcopTxtAttributes();
+ meshcopTxts.modelName = modelName;
+ meshcopTxts.vendorName = vendorName;
+ meshcopTxts.vendorOui = HexEncoding.decode(vendorOui.replace("-", "").replace(":", ""));
+ return meshcopTxts;
+ }
+
private void onOtDaemonDied() {
- Log.w(TAG, "OT daemon became dead, clean up...");
+ checkOnHandlerThread();
+ Log.w(TAG, "OT daemon is dead, clean up...");
+
OperationReceiverWrapper.onOtDaemonDied();
mOtDaemonCallbackProxy.onOtDaemonDied();
+ mTunIfController.onOtDaemonDied();
+ mNsdPublisher.onOtDaemonDied();
mOtDaemon = null;
+ maybeInitializeOtDaemon();
}
public void initialize() {
mHandler.post(
() -> {
- Log.d(TAG, "Initializing Thread system service...");
+ Log.d(
+ TAG,
+ "Initializing Thread system service: Thread is "
+ + (isEnabled() ? "enabled" : "disabled"));
try {
mTunIfController.createTunInterface();
} catch (IOException e) {
throw new IllegalStateException(
"Failed to create Thread tunnel interface", e);
}
- mLinkProperties.setInterfaceName(TUN_IF_NAME);
- mLinkProperties.setMtu(TunInterfaceController.MTU);
mConnectivityManager.registerNetworkProvider(mNetworkProvider);
requestUpstreamNetwork();
-
- initializeOtDaemon();
+ requestThreadNetwork();
+ mUserRestricted = isThreadUserRestricted();
+ registerUserRestrictionsReceiver();
+ maybeInitializeOtDaemon();
});
}
+ /**
+ * Force stops ot-daemon immediately and prevents ot-daemon from being restarted by
+ * system_server again.
+ *
+ * <p>This is for VTS testing only.
+ */
+ @RequiresPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED)
+ void forceStopOtDaemonForTest(boolean enabled, @NonNull IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ mHandler.post(
+ () ->
+ forceStopOtDaemonForTestInternal(
+ enabled,
+ new OperationReceiverWrapper(
+ receiver, true /* expectOtDaemonDied */)));
+ }
+
+ private void forceStopOtDaemonForTestInternal(
+ boolean enabled, @NonNull OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+ if (enabled == mForceStopOtDaemonEnabled) {
+ receiver.onSuccess();
+ return;
+ }
+
+ if (!enabled) {
+ mForceStopOtDaemonEnabled = false;
+ maybeInitializeOtDaemon();
+ receiver.onSuccess();
+ return;
+ }
+
+ try {
+ getOtDaemon().terminate();
+ // Do not invoke the {@code receiver} callback here but wait for ot-daemon to
+ // become dead, so that it's guaranteed that ot-daemon is stopped when {@code
+ // receiver} is completed
+ } catch (RemoteException e) {
+ Log.e(TAG, "otDaemon.terminate failed", e);
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ } catch (ThreadNetworkException e) {
+ // No ThreadNetworkException.ERROR_THREAD_DISABLED error will be thrown
+ throw new AssertionError(e);
+ } finally {
+ mForceStopOtDaemonEnabled = true;
+ }
+ }
+
+ public void setEnabled(boolean isEnabled, @NonNull IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ mHandler.post(
+ () ->
+ setEnabledInternal(
+ isEnabled,
+ true /* persist */,
+ new OperationReceiverWrapper(receiver)));
+ }
+
+ private void setEnabledInternal(
+ boolean isEnabled, boolean persist, @NonNull OperationReceiverWrapper receiver) {
+ if (isEnabled && isThreadUserRestricted()) {
+ receiver.onError(
+ ERROR_FAILED_PRECONDITION,
+ "Cannot enable Thread: forbidden by user restriction");
+ return;
+ }
+
+ Log.i(TAG, "Set Thread enabled: " + isEnabled + ", persist: " + persist);
+
+ if (persist) {
+ // The persistent setting keeps the desired enabled state, thus it's set regardless
+ // the otDaemon set enabled state operation succeeded or not, so that it can recover
+ // to the desired value after reboot.
+ mPersistentSettings.put(ThreadPersistentSettings.THREAD_ENABLED.key, isEnabled);
+ }
+
+ try {
+ getOtDaemon().setThreadEnabled(isEnabled, newOtStatusReceiver(receiver));
+ } catch (RemoteException | ThreadNetworkException e) {
+ Log.e(TAG, "otDaemon.setThreadEnabled failed", e);
+ receiver.onError(e);
+ }
+ }
+
+ private void registerUserRestrictionsReceiver() {
+ mContext.registerReceiver(
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ onUserRestrictionsChanged(isThreadUserRestricted());
+ }
+ },
+ new IntentFilter(UserManager.ACTION_USER_RESTRICTIONS_CHANGED),
+ null /* broadcastPermission */,
+ mHandler);
+ }
+
+ private void onUserRestrictionsChanged(boolean newUserRestrictedState) {
+ checkOnHandlerThread();
+ if (mUserRestricted == newUserRestrictedState) {
+ return;
+ }
+ Log.i(
+ TAG,
+ "Thread user restriction changed: "
+ + mUserRestricted
+ + " -> "
+ + newUserRestrictedState);
+ mUserRestricted = newUserRestrictedState;
+
+ final boolean isEnabled = isEnabled();
+ final IOperationReceiver receiver =
+ new IOperationReceiver.Stub() {
+ @Override
+ public void onSuccess() {
+ Log.d(
+ TAG,
+ (isEnabled ? "Enabled" : "Disabled")
+ + " Thread due to user restriction change");
+ }
+
+ @Override
+ public void onError(int otError, String messages) {
+ Log.e(
+ TAG,
+ "Failed to "
+ + (isEnabled ? "enable" : "disable")
+ + " Thread for user restriction change");
+ }
+ };
+ // Do not save the user restriction state to persistent settings so that the user
+ // configuration won't be overwritten
+ setEnabledInternal(isEnabled, false /* persist */, new OperationReceiverWrapper(receiver));
+ }
+
+ /** Returns {@code true} if Thread is set enabled. */
+ private boolean isEnabled() {
+ return !mForceStopOtDaemonEnabled
+ && !mUserRestricted
+ && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
+ }
+
+ /** Returns {@code true} if Thread has been restricted for the user. */
+ private boolean isThreadUserRestricted() {
+ return mUserManager.hasUserRestriction(DISALLOW_THREAD_NETWORK);
+ }
+
private void requestUpstreamNetwork() {
if (mUpstreamNetworkCallback != null) {
throw new AssertionError("The upstream network request is already there.");
@@ -360,25 +569,31 @@
@Override
public void onAvailable(@NonNull Network network) {
checkOnHandlerThread();
- Log.i(TAG, "onAvailable: " + network);
+ Log.i(TAG, "Upstream network available: " + network);
}
@Override
public void onLost(@NonNull Network network) {
checkOnHandlerThread();
- Log.i(TAG, "onLost: " + network);
+ Log.i(TAG, "Upstream network lost: " + network);
+
+ // TODO: disable border routing when upsteam network disconnected
}
@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());
+
+ String existingIfName = mNetworkToInterface.get(network);
+ String newIfName = linkProperties.getInterfaceName();
+ if (Objects.equals(existingIfName, newIfName)) {
+ return;
+ }
+ Log.i(TAG, "Upstream network changed: " + existingIfName + " -> " + newIfName);
+ mNetworkToInterface.put(network, newIfName);
+
+ // TODO: disable border routing if netIfName is null
if (network.equals(mUpstreamNetwork)) {
enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
}
@@ -389,16 +604,29 @@
@Override
public void onAvailable(@NonNull Network network) {
checkOnHandlerThread();
- Log.i(TAG, "onAvailable: Thread network Available");
+ Log.i(TAG, "Thread network is available: " + network);
+ }
+
+ @Override
+ public void onLost(@NonNull Network network) {
+ checkOnHandlerThread();
+ Log.i(TAG, "Thread network is lost: " + network);
+ disableBorderRouting();
}
@Override
public void onLocalNetworkInfoChanged(
@NonNull Network network, @NonNull LocalNetworkInfo localNetworkInfo) {
checkOnHandlerThread();
- Log.i(TAG, "onLocalNetworkInfoChanged: " + localNetworkInfo);
+ Log.i(
+ TAG,
+ "LocalNetworkInfo of Thread network changed: {threadNetwork: "
+ + network
+ + ", localNetworkInfo: "
+ + localNetworkInfo
+ + "}");
if (localNetworkInfo.getUpstreamNetwork() == null) {
- mUpstreamNetwork = null;
+ disableBorderRouting();
return;
}
if (!localNetworkInfo.getUpstreamNetwork().equals(mUpstreamNetwork)) {
@@ -413,9 +641,11 @@
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)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
.build(),
new ThreadNetworkCallback(),
mHandler);
@@ -436,6 +666,7 @@
new NetworkCapabilities.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
.build();
final NetworkScore score =
@@ -447,7 +678,7 @@
mHandler.getLooper(),
TAG,
netCaps,
- mLinkProperties,
+ mTunIfController.getLinkProperties(),
newLocalNetworkConfig(),
score,
new NetworkAgentConfig.Builder().build(),
@@ -459,8 +690,6 @@
return;
}
- requestThreadNetwork();
-
mNetworkAgent = newNetworkAgent();
mNetworkAgent.register();
mNetworkAgent.markConnected();
@@ -480,46 +709,6 @@
mNetworkAgent = null;
}
- private void updateTunInterfaceAddress(LinkAddress linkAddress, boolean isAdded) {
- try {
- if (isAdded) {
- mTunIfController.addAddress(linkAddress);
- } else {
- mTunIfController.removeAddress(linkAddress);
- }
- } catch (IOException e) {
- Log.e(
- TAG,
- String.format(
- "Failed to %s Thread tun interface address %s",
- (isAdded ? "add" : "remove"), linkAddress),
- e);
- }
- }
-
- private void updateNetworkLinkProperties(LinkAddress linkAddress, boolean isAdded) {
- RouteInfo routeInfo =
- new RouteInfo(
- new IpPrefix(linkAddress.getAddress(), 64),
- null,
- TUN_IF_NAME,
- RouteInfo.RTN_UNICAST,
- TunInterfaceController.MTU);
- if (isAdded) {
- mLinkProperties.addLinkAddress(linkAddress);
- mLinkProperties.addRoute(routeInfo);
- } else {
- mLinkProperties.removeLinkAddress(linkAddress);
- mLinkProperties.removeRoute(routeInfo);
- }
-
- // The Thread daemon can send link property updates before the networkAgent is
- // registered
- if (mNetworkAgent != null) {
- mNetworkAgent.sendLinkProperties(mLinkProperties);
- }
- }
-
@Override
public int getThreadVersion() {
return THREAD_VERSION_1_3;
@@ -528,26 +717,51 @@
@Override
public void createRandomizedDataset(
String networkName, IActiveOperationalDatasetReceiver receiver) {
- mHandler.post(
- () -> {
- ActiveOperationalDataset dataset =
- createRandomizedDatasetInternal(
- networkName,
- mSupportedChannelMask,
- Instant.now(),
- new Random(),
- new SecureRandom());
- try {
- receiver.onSuccess(dataset);
- } catch (RemoteException e) {
- // The client is dead, do nothing
- }
- });
+ ActiveOperationalDatasetReceiverWrapper receiverWrapper =
+ new ActiveOperationalDatasetReceiverWrapper(receiver);
+ mHandler.post(() -> createRandomizedDatasetInternal(networkName, receiverWrapper));
}
- private static ActiveOperationalDataset createRandomizedDatasetInternal(
+ private void createRandomizedDatasetInternal(
+ String networkName, @NonNull ActiveOperationalDatasetReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().getChannelMasks(newChannelMasksReceiver(networkName, receiver));
+ } catch (RemoteException | ThreadNetworkException e) {
+ Log.e(TAG, "otDaemon.getChannelMasks failed", e);
+ receiver.onError(e);
+ }
+ }
+
+ private IChannelMasksReceiver newChannelMasksReceiver(
+ String networkName, ActiveOperationalDatasetReceiverWrapper receiver) {
+ return new IChannelMasksReceiver.Stub() {
+ @Override
+ public void onSuccess(int supportedChannelMask, int preferredChannelMask) {
+ ActiveOperationalDataset dataset =
+ createRandomizedDataset(
+ networkName,
+ supportedChannelMask,
+ preferredChannelMask,
+ Instant.now(),
+ new Random(),
+ new SecureRandom());
+
+ receiver.onSuccess(dataset);
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ receiver.onError(otErrorToAndroidError(errorCode), errorMessage);
+ }
+ };
+ }
+
+ private static ActiveOperationalDataset createRandomizedDataset(
String networkName,
int supportedChannelMask,
+ int preferredChannelMask,
Instant now,
Random random,
SecureRandom secureRandom) {
@@ -557,6 +771,7 @@
final SparseArray<byte[]> channelMask = new SparseArray<>(1);
channelMask.put(CHANNEL_PAGE_24_GHZ, channelMaskToByteArray(supportedChannelMask));
+ final int channel = selectChannel(supportedChannelMask, preferredChannelMask, random);
final byte[] securityFlags = new byte[] {(byte) 0xff, (byte) 0xf8};
@@ -567,7 +782,7 @@
.setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID))
.setPanId(panId)
.setNetworkName(networkName)
- .setChannel(CHANNEL_PAGE_24_GHZ, selectRandomChannel(supportedChannelMask, random))
+ .setChannel(CHANNEL_PAGE_24_GHZ, channel)
.setChannelMask(channelMask)
.setPskc(newRandomBytes(secureRandom, LENGTH_PSKC))
.setNetworkKey(newRandomBytes(secureRandom, LENGTH_NETWORK_KEY))
@@ -576,6 +791,26 @@
.build();
}
+ private static int selectChannel(
+ int supportedChannelMask, int preferredChannelMask, Random random) {
+ // Due to radio hardware performance reasons, many Thread radio chips need to reduce their
+ // transmit power on edge channels to pass regulatory RF certification. Thread edge channel
+ // 25 and 26 are not preferred here.
+ //
+ // If users want to use channel 25 or 26, they can change the channel via the method
+ // ActiveOperationalDataset.Builder(activeOperationalDataset).setChannel(channel).build().
+ preferredChannelMask = preferredChannelMask & CHANNEL_MASK_11_TO_24;
+
+ // If the preferred channel mask is not empty, select a random channel from it, otherwise
+ // choose one from the supported channel mask.
+ preferredChannelMask = preferredChannelMask & supportedChannelMask;
+ if (preferredChannelMask == 0) {
+ preferredChannelMask = supportedChannelMask;
+ }
+
+ return selectRandomChannel(preferredChannelMask, random);
+ }
+
private static byte[] newRandomBytes(Random random, int length) {
byte[] result = new byte[length];
random.nextBytes(result);
@@ -675,9 +910,8 @@
return ERROR_ABORTED;
case OT_ERROR_BUSY:
return ERROR_BUSY;
- case OT_ERROR_DETACHED:
- case OT_ERROR_INVALID_STATE:
- return ERROR_FAILED_PRECONDITION;
+ case OT_ERROR_NOT_IMPLEMENTED:
+ return ERROR_UNSUPPORTED_OPERATION;
case OT_ERROR_NO_BUFS:
return ERROR_RESOURCE_EXHAUSTED;
case OT_ERROR_PARSE:
@@ -689,6 +923,11 @@
return ERROR_REJECTED_BY_PEER;
case OT_ERROR_UNSUPPORTED_CHANNEL:
return ERROR_UNSUPPORTED_CHANNEL;
+ case OT_ERROR_THREAD_DISABLED:
+ return ERROR_THREAD_DISABLED;
+ case OT_ERROR_FAILED_PRECONDITION:
+ return ERROR_FAILED_PRECONDITION;
+ case OT_ERROR_INVALID_STATE:
default:
return ERROR_INTERNAL_ERROR;
}
@@ -711,9 +950,9 @@
try {
// The otDaemon.join() will leave first if this device is currently attached
getOtDaemon().join(activeDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
- } catch (RemoteException e) {
+ } catch (RemoteException | ThreadNetworkException e) {
Log.e(TAG, "otDaemon.join failed", e);
- receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ receiver.onError(e);
}
}
@@ -736,9 +975,9 @@
getOtDaemon()
.scheduleMigration(
pendingDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
- } catch (RemoteException e) {
+ } catch (RemoteException | ThreadNetworkException e) {
Log.e(TAG, "otDaemon.scheduleMigration failed", e);
- receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ receiver.onError(e);
}
}
@@ -754,9 +993,9 @@
try {
getOtDaemon().leave(newOtStatusReceiver(receiver));
- } catch (RemoteException e) {
- // Oneway AIDL API should never throw?
- receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ } catch (RemoteException | ThreadNetworkException e) {
+ Log.e(TAG, "otDaemon.leave failed", e);
+ receiver.onError(e);
}
}
@@ -778,10 +1017,73 @@
String countryCode, @NonNull OperationReceiverWrapper receiver) {
checkOnHandlerThread();
+ // Fails early to avoid waking up ot-daemon by the ThreadNetworkCountryCode class
+ if (!isEnabled()) {
+ receiver.onError(
+ ERROR_THREAD_DISABLED, "Can't set country code when Thread is disabled");
+ return;
+ }
+
try {
getOtDaemon().setCountryCode(countryCode, newOtStatusReceiver(receiver));
- } catch (RemoteException e) {
+ } catch (RemoteException | ThreadNetworkException e) {
Log.e(TAG, "otDaemon.setCountryCode failed", e);
+ receiver.onError(e);
+ }
+ }
+
+ @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
+ }
+ }
+
+ @RequiresPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED)
+ public void setChannelMaxPowers(
+ @NonNull ChannelMaxPower[] channelMaxPowers, @NonNull IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ mHandler.post(
+ () ->
+ setChannelMaxPowersInternal(
+ channelMaxPowers, new OperationReceiverWrapper(receiver)));
+ }
+
+ private void setChannelMaxPowersInternal(
+ @NonNull ChannelMaxPower[] channelMaxPowers,
+ @NonNull OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().setChannelMaxPowers(channelMaxPowers, newOtStatusReceiver(receiver));
+ } catch (RemoteException | ThreadNetworkException e) {
+ Log.e(TAG, "otDaemon.setChannelMaxPowers failed", e);
receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
}
}
@@ -791,38 +1093,39 @@
&& infraIfName.equals(mBorderRouterConfig.infraInterfaceName)) {
return;
}
- Log.i(TAG, "enableBorderRouting on AIL: " + infraIfName);
+ Log.i(TAG, "Enable border routing on AIL: " + infraIfName);
try {
mBorderRouterConfig.infraInterfaceName = infraIfName;
mBorderRouterConfig.infraInterfaceIcmp6Socket =
mInfraIfController.createIcmp6Socket(infraIfName);
mBorderRouterConfig.isBorderRoutingEnabled = true;
- mOtDaemon.configureBorderRouter(
- mBorderRouterConfig,
- new IOtStatusReceiver.Stub() {
- @Override
- public void onSuccess() {
- Log.i(TAG, "configure border router successfully");
- }
+ getOtDaemon()
+ .configureBorderRouter(
+ mBorderRouterConfig, new ConfigureBorderRouterStatusReceiver());
+ } catch (RemoteException | IOException | ThreadNetworkException e) {
+ Log.w(TAG, "Failed to enable border routing", e);
+ }
+ }
- @Override
- public void onError(int i, String s) {
- Log.w(
- TAG,
- String.format(
- "failed to configure border router: %d %s", i, s));
- }
- });
- } catch (Exception e) {
- Log.w(TAG, "enableBorderRouting failed: " + e);
+ private void disableBorderRouting() {
+ mUpstreamNetwork = null;
+ mBorderRouterConfig.infraInterfaceName = null;
+ mBorderRouterConfig.infraInterfaceIcmp6Socket = null;
+ mBorderRouterConfig.isBorderRoutingEnabled = false;
+ try {
+ getOtDaemon()
+ .configureBorderRouter(
+ mBorderRouterConfig, new ConfigureBorderRouterStatusReceiver());
+ } catch (RemoteException | ThreadNetworkException e) {
+ Log.w(TAG, "Failed to disable border routing", e);
}
}
private void handleThreadInterfaceStateChanged(boolean isUp) {
try {
mTunIfController.setInterfaceUp(isUp);
- Log.d(TAG, "Thread network interface becomes " + (isUp ? "up" : "down"));
+ Log.i(TAG, "Thread TUN interface becomes " + (isUp ? "up" : "down"));
} catch (IOException e) {
Log.e(TAG, "Failed to handle Thread interface state changes", e);
}
@@ -830,13 +1133,13 @@
private void handleDeviceRoleChanged(@DeviceRole int deviceRole) {
if (ThreadNetworkController.isAttached(deviceRole)) {
- Log.d(TAG, "Attached to the Thread network");
+ Log.i(TAG, "Attached to the Thread network");
// This is an idempotent method which can be called for multiple times when the device
// is already attached (e.g. going from Child to Router)
registerThreadNetwork();
} else {
- Log.d(TAG, "Detached from the Thread network");
+ Log.i(TAG, "Detached from the Thread network");
// This is an idempotent method which can be called for multiple times when the device
// is already detached or stopped
@@ -844,24 +1147,16 @@
}
}
- private void handleAddressChanged(Ipv6AddressInfo addressInfo, boolean isAdded) {
+ private void handleAddressChanged(List<Ipv6AddressInfo> addressInfoList) {
checkOnHandlerThread();
- InetAddress address = addressInfoToInetAddress(addressInfo);
- if (address.isMulticastAddress()) {
- Log.i(TAG, "Ignoring multicast address " + address.getHostAddress());
- return;
+
+ mTunIfController.updateAddresses(addressInfoList);
+
+ // The OT daemon can send link property updates before the networkAgent is
+ // registered
+ if (mNetworkAgent != null) {
+ mNetworkAgent.sendLinkProperties(mTunIfController.getLinkProperties());
}
-
- LinkAddress linkAddress = newLinkAddress(addressInfo);
- Log.d(TAG, (isAdded ? "Adding" : "Removing") + " address " + linkAddress);
-
- updateTunInterfaceAddress(linkAddress, isAdded);
- updateNetworkLinkProperties(linkAddress, isAdded);
- }
-
- private boolean isMulticastForwardingEnabled() {
- return !(mUpstreamMulticastRoutingConfig.getForwardingMode() == FORWARD_NONE
- && mDownstreamMulticastRoutingConfig.getForwardingMode() == FORWARD_NONE);
}
private void sendLocalNetworkConfig() {
@@ -873,73 +1168,44 @@
Log.d(TAG, "Sent localNetworkConfig: " + localNetworkConfig);
}
- private void handleMulticastForwardingStateChanged(boolean isEnabled) {
- if (isMulticastForwardingEnabled() == isEnabled) {
- return;
- }
- if (isEnabled) {
+ private void handleMulticastForwardingChanged(BackboneRouterState state) {
+ MulticastRoutingConfig upstreamMulticastRoutingConfig;
+ MulticastRoutingConfig downstreamMulticastRoutingConfig;
+
+ if (state.multicastForwardingEnabled) {
// When multicast forwarding is enabled, setup upstream forwarding to any address
// with minimal scope 4
// setup downstream forwarding with addresses subscribed from Thread network
- mUpstreamMulticastRoutingConfig =
+ upstreamMulticastRoutingConfig =
new MulticastRoutingConfig.Builder(FORWARD_WITH_MIN_SCOPE, 4).build();
- mDownstreamMulticastRoutingConfig =
- new MulticastRoutingConfig.Builder(FORWARD_SELECTED).build();
+ downstreamMulticastRoutingConfig =
+ buildDownstreamMulticastRoutingConfigSelected(state.listeningAddresses);
} else {
// When multicast forwarding is disabled, set both upstream and downstream
// forwarding config to FORWARD_NONE.
- mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
- mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+ upstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+ downstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
}
+
+ if (upstreamMulticastRoutingConfig.equals(mUpstreamMulticastRoutingConfig)
+ && downstreamMulticastRoutingConfig.equals(mDownstreamMulticastRoutingConfig)) {
+ return;
+ }
+
+ mUpstreamMulticastRoutingConfig = upstreamMulticastRoutingConfig;
+ mDownstreamMulticastRoutingConfig = downstreamMulticastRoutingConfig;
sendLocalNetworkConfig();
- Log.d(
- TAG,
- "Sent updated localNetworkConfig with multicast forwarding "
- + (isEnabled ? "enabled" : "disabled"));
}
- private void handleMulticastForwardingAddressChanged(byte[] addressBytes, boolean isAdded) {
- Inet6Address address = bytesToInet6Address(addressBytes);
- MulticastRoutingConfig newDownstreamConfig;
- MulticastRoutingConfig.Builder builder;
-
- if (mDownstreamMulticastRoutingConfig.getForwardingMode()
- != MulticastRoutingConfig.FORWARD_SELECTED) {
- Log.e(
- TAG,
- "Ignore multicast listening address updates when downstream multicast "
- + "forwarding mode is not FORWARD_SELECTED");
- // Don't update the address set if downstream multicast forwarding is disabled.
- return;
- }
- if (isAdded
- == mDownstreamMulticastRoutingConfig.getListeningAddresses().contains(address)) {
- return;
- }
-
- builder = new MulticastRoutingConfig.Builder(FORWARD_SELECTED);
- for (Inet6Address listeningAddress :
- mDownstreamMulticastRoutingConfig.getListeningAddresses()) {
- builder.addListeningAddress(listeningAddress);
- }
-
- if (isAdded) {
+ private MulticastRoutingConfig buildDownstreamMulticastRoutingConfigSelected(
+ List<String> listeningAddresses) {
+ MulticastRoutingConfig.Builder builder =
+ new MulticastRoutingConfig.Builder(FORWARD_SELECTED);
+ for (String addressStr : listeningAddresses) {
+ Inet6Address address = (Inet6Address) InetAddresses.parseNumericAddress(addressStr);
builder.addListeningAddress(address);
- } else {
- builder.clearListeningAddress(address);
}
-
- newDownstreamConfig = builder.build();
- if (!newDownstreamConfig.equals(mDownstreamMulticastRoutingConfig)) {
- Log.d(
- TAG,
- "Multicast listening address "
- + address.getHostAddress()
- + " is "
- + (isAdded ? "added" : "removed"));
- mDownstreamMulticastRoutingConfig = newDownstreamConfig;
- sendLocalNetworkConfig();
- }
+ return builder.build();
}
private static final class CallbackMetadata {
@@ -963,6 +1229,20 @@
}
}
+ private static final class ConfigureBorderRouterStatusReceiver extends IOtStatusReceiver.Stub {
+ public ConfigureBorderRouterStatusReceiver() {}
+
+ @Override
+ public void onSuccess() {
+ Log.i(TAG, "Configured border router successfully");
+ }
+
+ @Override
+ public void onError(int i, String s) {
+ Log.w(TAG, String.format("Failed to configure border router: %d %s", i, s));
+ }
+ }
+
/**
* Handles and forwards Thread daemon callbacks. This class must be accessed from the thread of
* {@code mHandler}.
@@ -995,8 +1275,17 @@
try {
getOtDaemon().registerStateCallback(this, callbackMetadata.id);
- } catch (RemoteException e) {
- // oneway operation should never fail
+ } catch (RemoteException | ThreadNetworkException e) {
+ Log.e(TAG, "otDaemon.registerStateCallback failed", e);
+ }
+ }
+
+ private void notifyThreadEnabledUpdated(IStateCallback callback, int enabledState) {
+ try {
+ callback.onThreadEnableStateChanged(enabledState);
+ Log.i(TAG, "onThreadEnableStateChanged " + enabledState);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
}
}
@@ -1027,8 +1316,8 @@
try {
getOtDaemon().registerStateCallback(this, callbackMetadata.id);
- } catch (RemoteException e) {
- // oneway operation should never fail
+ } catch (RemoteException | ThreadNetworkException e) {
+ Log.e(TAG, "otDaemon.registerStateCallback failed", e);
}
}
@@ -1047,8 +1336,11 @@
return;
}
+ final int deviceRole = mState.deviceRole;
+ mState = null;
+
// If this device is already STOPPED or DETACHED, do nothing
- if (!ThreadNetworkController.isAttached(mState.deviceRole)) {
+ if (!ThreadNetworkController.isAttached(deviceRole)) {
return;
}
@@ -1064,6 +1356,31 @@
}
@Override
+ public void onThreadEnabledChanged(int state) {
+ mHandler.post(() -> onThreadEnabledChangedInternal(state));
+ }
+
+ private void onThreadEnabledChangedInternal(int state) {
+ checkOnHandlerThread();
+ for (IStateCallback callback : mStateCallbacks.keySet()) {
+ notifyThreadEnabledUpdated(callback, otStateToAndroidState(state));
+ }
+ }
+
+ private static int otStateToAndroidState(int state) {
+ switch (state) {
+ case OT_STATE_ENABLED:
+ return STATE_ENABLED;
+ case OT_STATE_DISABLED:
+ return STATE_DISABLED;
+ case OT_STATE_DISABLING:
+ return STATE_DISABLING;
+ default:
+ throw new IllegalArgumentException("Unknown ot state " + state);
+ }
+ }
+
+ @Override
public void onStateChanged(OtDaemonState newState, long listenerId) {
mHandler.post(() -> onStateChangedInternal(newState, listenerId));
}
@@ -1073,7 +1390,6 @@
onInterfaceStateChanged(newState.isInterfaceUp);
onDeviceRoleChanged(newState.deviceRole, listenerId);
onPartitionIdChanged(newState.partitionId, listenerId);
- onMulticastForwardingStateChanged(newState.multicastForwardingEnabled);
mState = newState;
ActiveOperationalDataset newActiveDataset;
@@ -1182,19 +1498,14 @@
}
}
- private void onMulticastForwardingStateChanged(boolean isEnabled) {
- checkOnHandlerThread();
- handleMulticastForwardingStateChanged(isEnabled);
+ @Override
+ public void onAddressChanged(List<Ipv6AddressInfo> addressInfoList) {
+ mHandler.post(() -> handleAddressChanged(addressInfoList));
}
@Override
- public void onAddressChanged(Ipv6AddressInfo addressInfo, boolean isAdded) {
- mHandler.post(() -> handleAddressChanged(addressInfo, isAdded));
- }
-
- @Override
- public void onMulticastForwardingAddressChanged(byte[] address, boolean isAdded) {
- mHandler.post(() -> handleMulticastForwardingAddressChanged(address, isAdded));
+ public void onBackboneRouterStateChanged(BackboneRouterState state) {
+ mHandler.post(() -> handleMulticastForwardingChanged(state));
}
}
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
index b7b6233..a194114 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
@@ -16,6 +16,8 @@
package com.android.server.thread;
+import static com.android.server.thread.ThreadPersistentSettings.THREAD_COUNTRY_CODE;
+
import android.annotation.Nullable;
import android.annotation.StringDef;
import android.annotation.TargetApi;
@@ -31,6 +33,7 @@
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback;
import android.os.Build;
+import android.sysprop.ThreadNetworkProperties;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
@@ -53,8 +56,8 @@
/**
* 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.
+ * location, WiFi, telephony or OEM configuration. This class sends Country Code to Thread Network
+ * native layer.
*
* <p>This class is thread-safe.
*/
@@ -77,19 +80,23 @@
value = {
COUNTRY_CODE_SOURCE_DEFAULT,
COUNTRY_CODE_SOURCE_LOCATION,
+ COUNTRY_CODE_SOURCE_OEM,
COUNTRY_CODE_SOURCE_OVERRIDE,
COUNTRY_CODE_SOURCE_TELEPHONY,
COUNTRY_CODE_SOURCE_TELEPHONY_LAST,
COUNTRY_CODE_SOURCE_WIFI,
+ COUNTRY_CODE_SOURCE_SETTINGS,
})
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_OEM = "Oem";
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 String COUNTRY_CODE_SOURCE_SETTINGS = "Settings";
private static final CountryCodeInfo DEFAULT_COUNTRY_CODE_INFO =
new CountryCodeInfo(DEFAULT_COUNTRY_CODE, COUNTRY_CODE_SOURCE_DEFAULT);
@@ -104,6 +111,7 @@
private final SubscriptionManager mSubscriptionManager;
private final Map<Integer, TelephonyCountryCodeSlotInfo> mTelephonyCountryCodeSlotInfoMap =
new ArrayMap();
+ private final ThreadPersistentSettings mPersistentSettings;
@Nullable private CountryCodeInfo mCurrentCountryCodeInfo;
@Nullable private CountryCodeInfo mLocationCountryCodeInfo;
@@ -111,6 +119,7 @@
@Nullable private CountryCodeInfo mWifiCountryCodeInfo;
@Nullable private CountryCodeInfo mTelephonyCountryCodeInfo;
@Nullable private CountryCodeInfo mTelephonyLastCountryCodeInfo;
+ @Nullable private CountryCodeInfo mOemCountryCodeInfo;
/** Container class to store Thread country code information. */
private static final class CountryCodeInfo {
@@ -118,13 +127,35 @@
@CountryCodeSource private String mSource;
private final Instant mUpdatedTimestamp;
+ /**
+ * Constructs a new {@code CountryCodeInfo} from the given country code, country code source
+ * and country coode created time.
+ *
+ * @param countryCode a String representation of the country code as defined in ISO 3166.
+ * @param countryCodeSource a String representation of country code source.
+ * @param instant a Instant representation of the time when the country code was created.
+ * @throws IllegalArgumentException if {@code countryCode} contains invalid country code.
+ */
public CountryCodeInfo(
String countryCode, @CountryCodeSource String countryCodeSource, Instant instant) {
+ if (!isValidCountryCode(countryCode)) {
+ throw new IllegalArgumentException("Country code is invalid: " + countryCode);
+ }
+
mCountryCode = countryCode;
mSource = countryCodeSource;
mUpdatedTimestamp = instant;
}
+ /**
+ * Constructs a new {@code CountryCodeInfo} from the given country code, country code
+ * source. The updated timestamp of the country code will be set to the time when {@code
+ * CountryCodeInfo} was constructed.
+ *
+ * @param countryCode a String representation of the country code as defined in ISO 3166.
+ * @param countryCodeSource a String representation of country code source.
+ * @throws IllegalArgumentException if {@code countryCode} contains invalid country code.
+ */
public CountryCodeInfo(String countryCode, @CountryCodeSource String countryCodeSource) {
this(countryCode, countryCodeSource, Instant.now());
}
@@ -188,7 +219,9 @@
WifiManager wifiManager,
Context context,
TelephonyManager telephonyManager,
- SubscriptionManager subscriptionManager) {
+ SubscriptionManager subscriptionManager,
+ @Nullable String oemCountryCode,
+ ThreadPersistentSettings persistentSettings) {
mLocationManager = locationManager;
mThreadNetworkControllerService = threadNetworkControllerService;
mGeocoder = geocoder;
@@ -197,10 +230,19 @@
mContext = context;
mTelephonyManager = telephonyManager;
mSubscriptionManager = subscriptionManager;
+ mPersistentSettings = persistentSettings;
+
+ if (oemCountryCode != null) {
+ mOemCountryCodeInfo = new CountryCodeInfo(oemCountryCode, COUNTRY_CODE_SOURCE_OEM);
+ }
+
+ mCurrentCountryCodeInfo = pickCountryCode();
}
public static ThreadNetworkCountryCode newInstance(
- Context context, ThreadNetworkControllerService controllerService) {
+ Context context,
+ ThreadNetworkControllerService controllerService,
+ ThreadPersistentSettings persistentSettings) {
return new ThreadNetworkCountryCode(
context.getSystemService(LocationManager.class),
controllerService,
@@ -209,7 +251,9 @@
context.getSystemService(WifiManager.class),
context,
context.getSystemService(TelephonyManager.class),
- context.getSystemService(SubscriptionManager.class));
+ context.getSystemService(SubscriptionManager.class),
+ ThreadNetworkProperties.country_code().orElse(null),
+ persistentSettings);
}
/** Sets up this country code module to listen to location country code changes. */
@@ -278,7 +322,14 @@
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);
+ if (isValidCountryCode(countryCode)) {
+ mWifiCountryCodeInfo =
+ new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_WIFI);
+ } else {
+ Log.w(TAG, "WiFi country code " + countryCode + " is invalid");
+ mWifiCountryCodeInfo = null;
+ }
+
updateCountryCode(false /* forceUpdate */);
}
}
@@ -418,6 +469,9 @@
* number will be used.
* <li>5. Location country code - Country code retrieved from LocationManager passive location
* provider.
+ * <li>6. OEM country code - Country code retrieved from the system property
+ * `threadnetwork.country_code`.
+ * <li>7. Default country code `WW`.
* </ul>
*
* @return the selected country code information.
@@ -443,6 +497,15 @@
return mLocationCountryCodeInfo;
}
+ String settingsCountryCode = mPersistentSettings.get(THREAD_COUNTRY_CODE);
+ if (settingsCountryCode != null) {
+ return new CountryCodeInfo(settingsCountryCode, COUNTRY_CODE_SOURCE_SETTINGS);
+ }
+
+ if (mOemCountryCodeInfo != null) {
+ return mOemCountryCodeInfo;
+ }
+
return DEFAULT_COUNTRY_CODE_INFO;
}
@@ -452,6 +515,8 @@
public void onSuccess() {
synchronized ("ThreadNetworkCountryCode.this") {
mCurrentCountryCodeInfo = countryCodeInfo;
+ mPersistentSettings.put(
+ THREAD_COUNTRY_CODE.key, countryCodeInfo.getCountryCode());
}
}
@@ -490,10 +555,9 @@
newOperationReceiver(countryCodeInfo));
}
- /** Returns the current country code or {@code null} if no country code is set. */
- @Nullable
+ /** Returns the current country code. */
public synchronized String getCountryCode() {
- return (mCurrentCountryCodeInfo != null) ? mCurrentCountryCodeInfo.getCountryCode() : null;
+ return mCurrentCountryCodeInfo.getCountryCode();
}
/**
@@ -531,13 +595,14 @@
/** 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("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("mTelephonyCountryCodeInfo : " + mTelephonyCountryCodeInfo);
+ pw.println("mWifiCountryCodeInfo : " + mWifiCountryCodeInfo);
+ pw.println("mTelephonyLastCountryCodeInfo : " + mTelephonyLastCountryCodeInfo);
+ pw.println("mLocationCountryCodeInfo : " + mLocationCountryCodeInfo);
+ pw.println("mOemCountryCodeInfo : " + mOemCountryCodeInfo);
+ 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 a3cf278..30c67ca 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
@@ -18,6 +18,8 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static java.util.Objects.requireNonNull;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -40,26 +42,37 @@
private final Context mContext;
@Nullable private ThreadNetworkCountryCode mCountryCode;
@Nullable private ThreadNetworkControllerService mControllerService;
+ private final ThreadPersistentSettings mPersistentSettings;
@Nullable private ThreadNetworkShellCommand mShellCommand;
/** Creates a new {@link ThreadNetworkService} object. */
public ThreadNetworkService(Context context) {
mContext = context;
+ mPersistentSettings = ThreadPersistentSettings.newInstance(context);
}
/**
- * 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) {
- mControllerService = ThreadNetworkControllerService.newInstance(mContext);
+ if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
+ mPersistentSettings.initialize();
+ mControllerService =
+ ThreadNetworkControllerService.newInstance(
+ mContext, mPersistentSettings, () -> mCountryCode.getCountryCode());
+ mCountryCode =
+ ThreadNetworkCountryCode.newInstance(
+ mContext, mControllerService, mPersistentSettings);
mControllerService.initialize();
- mCountryCode = ThreadNetworkCountryCode.newInstance(mContext, mControllerService);
+ } 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.initialize();
-
- mShellCommand = new ThreadNetworkShellCommand(mCountryCode);
+ mShellCommand =
+ new ThreadNetworkShellCommand(requireNonNull(mControllerService), mCountryCode);
}
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
index c17c5a7..c6a1618 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
@@ -16,7 +16,10 @@
package com.android.server.thread;
+import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.net.thread.IOperationReceiver;
+import android.net.thread.ThreadNetworkException;
import android.os.Binder;
import android.os.Process;
import android.text.TextUtils;
@@ -25,7 +28,12 @@
import com.android.modules.utils.BasicShellCommandHandler;
import java.io.PrintWriter;
+import java.time.Duration;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
/**
* Interprets and executes 'adb shell cmd thread_network [args]'.
@@ -37,16 +45,22 @@
* corresponding API permissions.
*/
public class ThreadNetworkShellCommand extends BasicShellCommandHandler {
- private static final String TAG = "ThreadNetworkShellCommand";
+ private static final Duration SET_ENABLED_TIMEOUT = Duration.ofSeconds(2);
+ private static final Duration FORCE_STOP_TIMEOUT = Duration.ofSeconds(1);
// These don't require root access.
- private static final List<String> NON_PRIVILEGED_COMMANDS = List.of("help", "get-country-code");
+ private static final List<String> NON_PRIVILEGED_COMMANDS =
+ List.of("help", "get-country-code", "enable", "disable");
- @Nullable private final ThreadNetworkCountryCode mCountryCode;
+ @NonNull private final ThreadNetworkControllerService mControllerService;
+ @NonNull private final ThreadNetworkCountryCode mCountryCode;
@Nullable private PrintWriter mOutputWriter;
@Nullable private PrintWriter mErrorWriter;
- ThreadNetworkShellCommand(@Nullable ThreadNetworkCountryCode countryCode) {
+ ThreadNetworkShellCommand(
+ @NonNull ThreadNetworkControllerService controllerService,
+ @NonNull ThreadNetworkCountryCode countryCode) {
+ mControllerService = controllerService;
mCountryCode = countryCode;
}
@@ -91,14 +105,14 @@
}
switch (cmd) {
+ case "enable":
+ return setThreadEnabled(true);
+ case "disable":
+ return setThreadEnabled(false);
+ case "force-stop-ot-daemon":
+ return forceStopOtDaemon();
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) {
@@ -124,11 +138,6 @@
}
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:
@@ -136,6 +145,64 @@
}
}
+ private int setThreadEnabled(boolean enabled) {
+ CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
+ mControllerService.setEnabled(enabled, newOperationReceiver(setEnabledFuture));
+ return waitForFuture(setEnabledFuture, FORCE_STOP_TIMEOUT, getErrorWriter());
+ }
+
+ private int forceStopOtDaemon() {
+ final PrintWriter errorWriter = getErrorWriter();
+ boolean enabled;
+ try {
+ enabled = getNextArgRequiredTrueOrFalse("enabled", "disabled");
+ } catch (IllegalArgumentException e) {
+ errorWriter.println("Invalid argument: " + e.getMessage());
+ return -1;
+ }
+
+ CompletableFuture<Void> forceStopFuture = new CompletableFuture<>();
+ mControllerService.forceStopOtDaemonForTest(enabled, newOperationReceiver(forceStopFuture));
+ return waitForFuture(forceStopFuture, FORCE_STOP_TIMEOUT, getErrorWriter());
+ }
+
+ private static IOperationReceiver newOperationReceiver(CompletableFuture<Void> future) {
+ return new IOperationReceiver.Stub() {
+ @Override
+ public void onSuccess() {
+ future.complete(null);
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ future.completeExceptionally(new ThreadNetworkException(errorCode, errorMessage));
+ }
+ };
+ }
+
+ /**
+ * Waits for the future to complete within given timeout.
+ *
+ * <p>Returns 0 if {@code future} completed successfully, or -1 if {@code future} failed to
+ * complete. When failed, error messages are printed to {@code errorWriter}.
+ */
+ private int waitForFuture(
+ CompletableFuture<Void> future, Duration timeout, PrintWriter errorWriter) {
+ try {
+ future.get(timeout.toSeconds(), TimeUnit.SECONDS);
+ return 0;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ errorWriter.println("Failed: " + e.getMessage());
+ } catch (ExecutionException e) {
+ errorWriter.println("Failed: " + e.getCause().getMessage());
+ } catch (TimeoutException e) {
+ errorWriter.println("Failed: command timeout for " + timeout);
+ }
+
+ return -1;
+ }
+
private static boolean argTrueOrFalse(String arg, String trueString, String falseString) {
if (trueString.equals(arg)) {
return true;
@@ -159,6 +226,10 @@
}
private void onHelpNonPrivileged(PrintWriter pw) {
+ pw.println(" enable");
+ pw.println(" Enables Thread radio");
+ pw.println(" disable");
+ pw.println(" Disables Thread radio");
pw.println(" get-country-code");
pw.println(" Gets country code as a two-letter string");
}
@@ -166,6 +237,8 @@
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");
+ pw.println(" force-stop-ot-daemon enabled | disabled ");
+ pw.println(" force stop ot-daemon service");
}
@Override
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index d32f0bf..8aaff60 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -16,15 +16,23 @@
package com.android.server.thread;
+import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+
import android.annotation.Nullable;
+import android.content.ApexEnvironment;
+import android.content.Context;
import android.os.PersistableBundle;
import android.util.AtomicFile;
import android.util.Log;
+import com.android.connectivity.resources.R;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.ConnectivityResources;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
@@ -38,10 +46,13 @@
*/
public class ThreadPersistentSettings {
private static final String TAG = "ThreadPersistentSettings";
+
/** File name used for storing settings. */
- public static final String FILE_NAME = "ThreadPersistentSettings.xml";
+ private 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.
@@ -50,7 +61,10 @@
/******** 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);
+ public static final Key<Boolean> THREAD_ENABLED = new Key<>("thread_enabled", true);
+
+ /** Stores the Thread country code, null if no country code is stored. */
+ public static final Key<String> THREAD_COUNTRY_CODE = new Key<>("thread_country_code", null);
/******** Thread persistent setting keys ***************/
@@ -62,16 +76,29 @@
@GuardedBy("mLock")
private final PersistableBundle mSettings = new PersistableBundle();
- public ThreadPersistentSettings(AtomicFile atomicFile) {
+ private final ConnectivityResources mResources;
+
+ public static ThreadPersistentSettings newInstance(Context context) {
+ return new ThreadPersistentSettings(
+ new AtomicFile(new File(getOrCreateThreadNetworkDir(), FILE_NAME)),
+ new ConnectivityResources(context));
+ }
+
+ @VisibleForTesting
+ ThreadPersistentSettings(AtomicFile atomicFile, ConnectivityResources resources) {
mAtomicFile = atomicFile;
+ mResources = resources;
}
/** 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);
+ if (!mSettings.containsKey(THREAD_ENABLED.key)) {
+ Log.i(TAG, "\"thread_enabled\" is missing in settings file, using default value");
+ put(
+ THREAD_ENABLED.key,
+ mResources.get().getBoolean(R.bool.config_thread_default_enabled));
}
}
}
@@ -99,7 +126,9 @@
private <T> T getObject(String key, T defaultValue) {
Object value;
synchronized (mLock) {
- if (defaultValue instanceof Boolean) {
+ if (defaultValue == null) {
+ value = mSettings.getString(key, null);
+ } else if (defaultValue instanceof Boolean) {
value = mSettings.getBoolean(key, (Boolean) defaultValue);
} else if (defaultValue instanceof Integer) {
value = mSettings.getInt(key, (Integer) defaultValue);
@@ -189,7 +218,7 @@
mSettings.putAll(bundleRead);
}
} catch (FileNotFoundException e) {
- Log.e(TAG, "No store file to read", e);
+ Log.w(TAG, "No store file to read", e);
} catch (IOException e) {
Log.e(TAG, "Read from store file failed", e);
}
@@ -240,4 +269,19 @@
throw e;
}
}
+
+ /** Get device protected storage dir for the tethering apex. */
+ private static File getOrCreateThreadNetworkDir() {
+ final File threadnetworkDir;
+ final File apexDataDir =
+ ApexEnvironment.getApexEnvironment(TETHERING_MODULE_NAME)
+ .getDeviceProtectedDataDir();
+ threadnetworkDir = new File(apexDataDir, "thread");
+
+ if (threadnetworkDir.exists() || threadnetworkDir.mkdirs()) {
+ return threadnetworkDir;
+ }
+ throw new IllegalStateException(
+ "Cannot write into thread network data directory: " + threadnetworkDir);
+ }
}
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index 7223b2a..97cdd55 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -17,7 +17,10 @@
package com.android.server.thread;
import android.annotation.Nullable;
+import android.net.IpPrefix;
import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.RouteInfo;
import android.net.util.SocketUtils;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
@@ -26,11 +29,19 @@
import android.system.OsConstants;
import android.util.Log;
+import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.net.module.util.netlink.RtNetlinkAddressMessage;
+import com.android.server.thread.openthread.Ipv6AddressInfo;
import java.io.FileDescriptor;
import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
/** Controller for virtual/tunnel network interfaces. */
public class TunInterfaceController {
@@ -43,13 +54,21 @@
}
private final String mIfName;
+ private final LinkProperties mLinkProperties = new LinkProperties();
private ParcelFileDescriptor mParcelTunFd;
private FileDescriptor mNetlinkSocket;
private static int sNetlinkSeqNo = 0;
/** Creates a new {@link TunInterfaceController} instance for given interface. */
public TunInterfaceController(String interfaceName) {
- this.mIfName = interfaceName;
+ mIfName = interfaceName;
+ mLinkProperties.setInterfaceName(mIfName);
+ mLinkProperties.setMtu(MTU);
+ }
+
+ /** Returns link properties of the Thread TUN interface. */
+ public LinkProperties getLinkProperties() {
+ return mLinkProperties;
}
/**
@@ -87,13 +106,18 @@
/** Sets the interface up or down according to {@code isUp}. */
public void setInterfaceUp(boolean isUp) throws IOException {
+ if (!isUp) {
+ for (LinkAddress address : mLinkProperties.getAllLinkAddresses()) {
+ removeAddress(address);
+ }
+ }
nativeSetInterfaceUp(mIfName, isUp);
}
private native void nativeSetInterfaceUp(String interfaceName, boolean isUp) throws IOException;
/** Adds a new address to the interface. */
- public void addAddress(LinkAddress address) throws IOException {
+ public void addAddress(LinkAddress address) {
Log.d(TAG, "Adding address " + address + " with flags: " + address.getFlags());
long validLifetimeSeconds;
@@ -121,7 +145,7 @@
byte[] message =
RtNetlinkAddressMessage.newRtmNewAddressMessage(
- sNetlinkSeqNo,
+ sNetlinkSeqNo++,
address.getAddress(),
(short) address.getPrefixLength(),
address.getFlags(),
@@ -131,13 +155,123 @@
preferredLifetimeSeconds);
try {
Os.write(mNetlinkSocket, message, 0, message.length);
- } catch (ErrnoException e) {
- throw new IOException("Failed to send netlink message", e);
+ } catch (ErrnoException | InterruptedIOException e) {
+ Log.e(TAG, "Failed to add address " + address, e);
+ return;
}
+ mLinkProperties.addLinkAddress(address);
+ mLinkProperties.addRoute(getRouteForAddress(address));
}
/** Removes an address from the interface. */
- public void removeAddress(LinkAddress address) throws IOException {
- // TODO(b/263222068): remove address with netlink
+ public void removeAddress(LinkAddress address) {
+ Log.d(TAG, "Removing address " + address);
+ byte[] message =
+ RtNetlinkAddressMessage.newRtmDelAddressMessage(
+ sNetlinkSeqNo++,
+ address.getAddress(),
+ (short) address.getPrefixLength(),
+ Os.if_nametoindex(mIfName));
+
+ // Intentionally update the mLinkProperties before send netlink message because the
+ // address is already removed from ot-daemon and apps can't reach to the address even
+ // when the netlink request below fails
+ mLinkProperties.removeLinkAddress(address);
+ mLinkProperties.removeRoute(getRouteForAddress(address));
+ try {
+ Os.write(mNetlinkSocket, message, 0, message.length);
+ } catch (ErrnoException | InterruptedIOException e) {
+ Log.e(TAG, "Failed to remove address " + address, e);
+ }
+ }
+
+ public void updateAddresses(List<Ipv6AddressInfo> addressInfoList) {
+ final List<LinkAddress> newLinkAddresses = new ArrayList<>();
+ boolean hasActiveOmrAddress = false;
+
+ for (Ipv6AddressInfo addressInfo : addressInfoList) {
+ if (addressInfo.isActiveOmr) {
+ hasActiveOmrAddress = true;
+ break;
+ }
+ }
+
+ for (Ipv6AddressInfo addressInfo : addressInfoList) {
+ InetAddress address = addressInfoToInetAddress(addressInfo);
+ if (address.isMulticastAddress()) {
+ // TODO: Logging here will create repeated logs for a single multicast address, and
+ // it currently is not mandatory for debugging. Add log for ignored multicast
+ // address when necessary.
+ continue;
+ }
+ newLinkAddresses.add(newLinkAddress(addressInfo, hasActiveOmrAddress));
+ }
+
+ final CompareResult<LinkAddress> addressDiff =
+ new CompareResult<>(mLinkProperties.getAllLinkAddresses(), newLinkAddresses);
+ for (LinkAddress linkAddress : addressDiff.removed) {
+ removeAddress(linkAddress);
+ }
+ for (LinkAddress linkAddress : addressDiff.added) {
+ addAddress(linkAddress);
+ }
+ }
+
+ private RouteInfo getRouteForAddress(LinkAddress linkAddress) {
+ return new RouteInfo(
+ new IpPrefix(linkAddress.getAddress(), linkAddress.getPrefixLength()),
+ null,
+ mIfName,
+ RouteInfo.RTN_UNICAST,
+ MTU);
+ }
+
+ /** Called by {@link ThreadNetworkControllerService} to do clean up when ot-daemon is dead. */
+ public void onOtDaemonDied() {
+ try {
+ setInterfaceUp(false);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to set Thread TUN interface down");
+ }
+ }
+
+ private static InetAddress addressInfoToInetAddress(Ipv6AddressInfo addressInfo) {
+ return bytesToInet6Address(addressInfo.address);
+ }
+
+ private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
+ try {
+ return (Inet6Address) Inet6Address.getByAddress(addressBytes);
+ } catch (UnknownHostException e) {
+ // This is unlikely to happen unless the Thread daemon is critically broken
+ return null;
+ }
+ }
+
+ private static LinkAddress newLinkAddress(
+ Ipv6AddressInfo addressInfo, boolean hasActiveOmrAddress) {
+ // Mesh-local addresses and OMR address have the same scope, to distinguish them we set
+ // mesh-local addresses as deprecated when there is an active OMR address.
+ // For OMR address and link-local address we only use the value isPreferred set by
+ // ot-daemon.
+ boolean isPreferred = addressInfo.isPreferred;
+ if (addressInfo.isMeshLocal && hasActiveOmrAddress) {
+ isPreferred = false;
+ }
+
+ final long deprecationTimeMillis =
+ isPreferred ? LinkAddress.LIFETIME_PERMANENT : SystemClock.elapsedRealtime();
+
+ final InetAddress address = addressInfoToInetAddress(addressInfo);
+
+ // flags and scope will be adjusted automatically depending on the address and
+ // its lifetimes.
+ return new LinkAddress(
+ address,
+ addressInfo.prefixLength,
+ 0 /* flags */,
+ 0 /* scope */,
+ deprecationTimeMillis,
+ LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
}
}
diff --git a/thread/service/proguard.flags b/thread/service/proguard.flags
deleted file mode 100644
index 5028982..0000000
--- a/thread/service/proguard.flags
+++ /dev/null
@@ -1,4 +0,0 @@
-# Ensure the callback methods are not stripped
--keepclassmembers class **.ThreadNetworkControllerService$ThreadNetworkCallback {
- *;
-}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 3cf31e5..8cdf38d 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -15,12 +15,12 @@
//
package {
+ default_team: "trendy_team_fwk_thread_network",
default_applicable_licenses: ["Android-Apache-2.0"],
}
android_test {
name: "CtsThreadNetworkTestCases",
- defaults: ["cts_defaults"],
min_sdk_version: "33",
sdk_version: "test_current",
manifest: "AndroidManifest.xml",
@@ -31,6 +31,7 @@
test_suites: [
"cts",
"general-tests",
+ "mcts-tethering",
"mts-tethering",
],
static_libs: [
@@ -40,14 +41,16 @@
"guava",
"guava-android-testlib",
"net-tests-utils",
+ "ThreadNetworkTestUtils",
"truth",
],
libs: [
"android.test.base",
"android.test.runner",
- "framework-connectivity-module-api-stubs-including-flagged"
+ "framework-connectivity-module-api-stubs-including-flagged",
],
// Test coverage system runs on different devices. Need to
// compile for all architectures.
compile_multilib: "both",
+ platform_apis: true,
}
diff --git a/thread/tests/cts/AndroidManifest.xml b/thread/tests/cts/AndroidManifest.xml
index 4370fe3..1541bf5 100644
--- a/thread/tests/cts/AndroidManifest.xml
+++ b/thread/tests/cts/AndroidManifest.xml
@@ -19,6 +19,9 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="android.net.thread.cts">
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
+
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
</application>
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 e02e74d..0e7f3be 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -16,24 +16,46 @@
package android.net.thread.cts;
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_ROUTER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
+import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
+import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
+import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
+import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
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;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
-import static org.junit.Assume.assumeNotNull;
+import static org.junit.Assert.fail;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
-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.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
@@ -42,83 +64,759 @@
import android.net.thread.ThreadNetworkController.StateCallback;
import android.net.thread.ThreadNetworkException;
import android.net.thread.ThreadNetworkManager;
-import android.os.Build;
+import android.net.thread.utils.TapTestNetworkTracker;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+import android.os.HandlerThread;
import android.os.OutcomeReceiver;
+import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import com.android.testutils.DevSdkIgnoreRunner;
-
-import com.google.common.util.concurrent.SettableFuture;
+import com.android.net.module.util.ArrayTrackRecord;
+import com.android.testutils.FunctionalUtils.ThrowingRunnable;
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
-import org.junit.runner.RunWith;
+import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Random;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
/** CTS tests for {@link ThreadNetworkController}. */
@LargeTest
-@RunWith(DevSdkIgnoreRunner.class)
-@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Thread is available on only U+
+@RequiresThreadFeature
public class ThreadNetworkControllerTest {
- private static final int CALLBACK_TIMEOUT_MILLIS = 1000;
- private static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
+ private static final int JOIN_TIMEOUT_MILLIS = 30 * 1000;
+ private static final int LEAVE_TIMEOUT_MILLIS = 2_000;
+ private static final int MIGRATION_TIMEOUT_MILLIS = 40 * 1_000;
+ private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000;
+ private static final int CALLBACK_TIMEOUT_MILLIS = 1_000;
+ private static final int ENABLED_TIMEOUT_MILLIS = 2_000;
+ private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 30_000;
+ private static final int SERVICE_LOST_TIMEOUT_MILLIS = 20_000;
+ private static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp";
+ private static final String THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
- @Rule public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+ @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
private final Context mContext = ApplicationProvider.getApplicationContext();
private ExecutorService mExecutor;
- private ThreadNetworkManager mManager;
+ private ThreadNetworkController mController;
+ private NsdManager mNsdManager;
private Set<String> mGrantedPermissions;
+ private HandlerThread mHandlerThread;
+ private TapTestNetworkTracker mTestNetworkTracker;
@Before
- public void setUp() {
- mExecutor = Executors.newSingleThreadExecutor();
- mManager = mContext.getSystemService(ThreadNetworkManager.class);
- mGrantedPermissions = new HashSet<String>();
+ public void setUp() throws Exception {
+ mController =
+ mContext.getSystemService(ThreadNetworkManager.class)
+ .getAllThreadNetworkControllers()
+ .get(0);
- // TODO: we will also need it in tearDown(), it's better to have a Rule to skip
- // tests if a feature is not available.
- assumeNotNull(mManager);
+ mGrantedPermissions = new HashSet<String>();
+ mExecutor = Executors.newSingleThreadExecutor();
+ mNsdManager = mContext.getSystemService(NsdManager.class);
+ mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+ mHandlerThread.start();
+
+ setEnabledAndWait(mController, true);
}
@After
public void tearDown() throws Exception {
- if (mManager != null) {
- leaveAndWait();
+ dropAllPermissions();
+ leaveAndWait(mController);
+ tearDownTestNetwork();
+ }
+
+ @Test
+ public void getThreadVersion_returnsAtLeastThreadVersion1P3() {
+ assertThat(mController.getThreadVersion()).isAtLeast(THREAD_VERSION_1_3);
+ }
+
+ @Test
+ public void registerStateCallback_permissionsGranted_returnsCurrentStates() throws Exception {
+ CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+ StateCallback callback = deviceRole::complete;
+
+ try {
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> mController.registerStateCallback(mExecutor, callback));
+
+ assertThat(deviceRole.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+ .isEqualTo(DEVICE_ROLE_STOPPED);
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+ }
+ }
+
+ @Test
+ public void registerStateCallback_returnsUpdatedEnabledStates() throws Exception {
+ CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+ EnabledStateListener listener = new EnabledStateListener(mController);
+
+ try {
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1));
+ });
+ setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mController.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture2));
+ });
+ setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+ listener.expectThreadEnabledState(STATE_ENABLED);
+ listener.expectThreadEnabledState(STATE_DISABLING);
+ listener.expectThreadEnabledState(STATE_DISABLED);
+ listener.expectThreadEnabledState(STATE_ENABLED);
+ } finally {
+ listener.unregisterStateCallback();
+ }
+ }
+
+ @Test
+ public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception {
+ dropAllPermissions();
+
+ assertThrows(
+ SecurityException.class,
+ () -> mController.registerStateCallback(mExecutor, role -> {}));
+ }
+
+ @Test
+ public void registerStateCallback_alreadyRegistered_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE);
+ CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+ StateCallback callback = role -> deviceRole.complete(role);
+
+ mController.registerStateCallback(mExecutor, callback);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.registerStateCallback(mExecutor, callback));
+ }
+
+ @Test
+ public void unregisterStateCallback_noPermissions_throwsSecurityException() throws Exception {
+ CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+ StateCallback callback = role -> deviceRole.complete(role);
+ runAsShell(
+ ACCESS_NETWORK_STATE, () -> mController.registerStateCallback(mExecutor, callback));
+
+ try {
dropAllPermissions();
+ assertThrows(
+ SecurityException.class, () -> mController.unregisterStateCallback(callback));
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
}
}
- private List<ThreadNetworkController> getAllControllers() {
- return mManager.getAllThreadNetworkControllers();
+ @Test
+ public void unregisterStateCallback_callbackRegistered_success() throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE);
+ CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+ StateCallback callback = role -> deviceRole.complete(role);
+
+ assertDoesNotThrow(() -> mController.registerStateCallback(mExecutor, callback));
+ mController.unregisterStateCallback(callback);
}
- private void leaveAndWait() throws Exception {
- grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ @Test
+ public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException()
+ throws Exception {
+ CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+ StateCallback callback = role -> deviceRole.complete(role);
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<Void> future = SettableFuture.create();
- controller.leave(mExecutor, future::set);
- future.get();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.unregisterStateCallback(callback));
+ }
+
+ @Test
+ public void unregisterStateCallback_alreadyUnregistered_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE);
+ CompletableFuture<Integer> deviceRole = new CompletableFuture<>();
+ StateCallback callback = deviceRole::complete;
+ mController.registerStateCallback(mExecutor, callback);
+ mController.unregisterStateCallback(callback);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.unregisterStateCallback(callback));
+ }
+
+ @Test
+ public void registerOperationalDatasetCallback_permissionsGranted_returnsCurrentStates()
+ throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+ CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+ CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+
+ try {
+ mController.registerOperationalDatasetCallback(mExecutor, callback);
+
+ assertThat(activeFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+ assertThat(pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+ } finally {
+ mController.unregisterOperationalDatasetCallback(callback);
}
}
+ @Test
+ public void registerOperationalDatasetCallback_noPermissions_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+ CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+ CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+
+ assertThrows(
+ SecurityException.class,
+ () -> mController.registerOperationalDatasetCallback(mExecutor, callback));
+ }
+
+ @Test
+ public void unregisterOperationalDatasetCallback_callbackRegistered_success() throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+ CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+ CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+ mController.registerOperationalDatasetCallback(mExecutor, callback);
+
+ assertDoesNotThrow(() -> mController.unregisterOperationalDatasetCallback(callback));
+ }
+
+ @Test
+ public void unregisterOperationalDatasetCallback_noPermissions_throwsSecurityException()
+ throws Exception {
+ CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+ CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.registerOperationalDatasetCallback(mExecutor, callback));
+
+ try {
+ dropAllPermissions();
+ assertThrows(
+ SecurityException.class,
+ () -> mController.unregisterOperationalDatasetCallback(callback));
+ } finally {
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.unregisterOperationalDatasetCallback(callback));
+ }
+ }
+
+ private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
+ CompletableFuture<V> future) {
+ return new OutcomeReceiver<V, ThreadNetworkException>() {
+ @Override
+ public void onResult(V result) {
+ future.complete(result);
+ }
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ future.completeExceptionally(e);
+ }
+ };
+ }
+
+ @Test
+ public void join_withPrivilegedPermission_success() throws Exception {
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+ joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+ assertThat(isAttached(mController)).isTrue();
+ assertThat(getActiveOperationalDataset(mController)).isEqualTo(activeDataset);
+ }
+
+ @Test
+ public void join_withoutPrivilegedPermission_throwsSecurityException() throws Exception {
+ dropAllPermissions();
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+
+ assertThrows(
+ SecurityException.class, () -> mController.join(activeDataset, mExecutor, v -> {}));
+ }
+
+ @Test
+ public void join_threadDisabled_failsWithErrorThreadDisabled() throws Exception {
+ setEnabledAndWait(mController, false);
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+
+ var thrown =
+ assertThrows(
+ ExecutionException.class,
+ () -> joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS));
+ var threadException = (ThreadNetworkException) thrown.getCause();
+ assertThat(threadException.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
+ }
+
+ @Test
+ public void join_concurrentRequests_firstOneIsAborted() throws Exception {
+ final byte[] KEY_1 = new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
+ final byte[] KEY_2 = new byte[] {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2};
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(newRandomizedDataset("TestNet", mController))
+ .setNetworkKey(KEY_1)
+ .build();
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset1).setNetworkKey(KEY_2).build();
+ CompletableFuture<Void> joinFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> joinFuture2 = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mController.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture1));
+ mController.join(activeDataset2, mExecutor, newOutcomeReceiver(joinFuture2));
+ });
+
+ var thrown =
+ assertThrows(
+ ExecutionException.class,
+ () -> joinFuture1.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS));
+ var threadException = (ThreadNetworkException) thrown.getCause();
+ assertThat(threadException.getErrorCode()).isEqualTo(ERROR_ABORTED);
+ joinFuture2.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+ assertThat(isAttached(mController)).isTrue();
+ assertThat(getActiveOperationalDataset(mController)).isEqualTo(activeDataset2);
+ }
+
+ @Test
+ public void leave_withPrivilegedPermission_success() throws Exception {
+ CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
+ joinRandomizedDatasetAndWait(mController);
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.leave(mExecutor, newOutcomeReceiver(leaveFuture)));
+ leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+
+ assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+ }
+
+ @Test
+ public void leave_withoutPrivilegedPermission_throwsSecurityException() {
+ dropAllPermissions();
+
+ assertThrows(SecurityException.class, () -> mController.leave(mExecutor, v -> {}));
+ }
+
+ @Test
+ public void leave_threadDisabled_success() throws Exception {
+ setEnabledAndWait(mController, false);
+ CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
+
+ leave(mController, newOutcomeReceiver(leaveFuture));
+ leaveFuture.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+
+ assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+ }
+
+ @Test
+ public void leave_concurrentRequests_bothSuccess() throws Exception {
+ CompletableFuture<Void> leaveFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> leaveFuture2 = new CompletableFuture<>();
+ joinRandomizedDatasetAndWait(mController);
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mController.leave(mExecutor, newOutcomeReceiver(leaveFuture1));
+ mController.leave(mExecutor, newOutcomeReceiver(leaveFuture2));
+ });
+
+ leaveFuture1.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+ leaveFuture2.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+ assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+ }
+
+ @Test
+ public void scheduleMigration_withPrivilegedPermission_newDatasetApplied() throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(newRandomizedDataset("TestNet", mController))
+ .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+ .setExtendedPanId(new byte[] {1, 1, 1, 1, 1, 1, 1, 1})
+ .build();
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset1)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+ .setNetworkName("ThreadNet2")
+ .build();
+ PendingOperationalDataset pendingDataset2 =
+ new PendingOperationalDataset(
+ activeDataset2,
+ OperationalDatasetTimestamp.fromInstant(Instant.now()),
+ Duration.ofSeconds(30));
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+ CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
+ mController.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+ mController.scheduleMigration(
+ pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture));
+ migrateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+
+ CompletableFuture<Boolean> dataset2IsApplied = new CompletableFuture<>();
+ CompletableFuture<Boolean> pendingDatasetIsRemoved = new CompletableFuture<>();
+ OperationalDatasetCallback datasetCallback =
+ new OperationalDatasetCallback() {
+ @Override
+ public void onActiveOperationalDatasetChanged(
+ ActiveOperationalDataset activeDataset) {
+ if (activeDataset.equals(activeDataset2)) {
+ dataset2IsApplied.complete(true);
+ }
+ }
+
+ @Override
+ public void onPendingOperationalDatasetChanged(
+ PendingOperationalDataset pendingDataset) {
+ if (pendingDataset == null) {
+ pendingDatasetIsRemoved.complete(true);
+ }
+ }
+ };
+ mController.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+ try {
+ assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+ assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+ } finally {
+ mController.unregisterOperationalDatasetCallback(datasetCallback);
+ }
+ }
+
+ @Test
+ public void scheduleMigration_whenNotAttached_failWithPreconditionError() throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+ PendingOperationalDataset pendingDataset =
+ new PendingOperationalDataset(
+ newRandomizedDataset("TestNet", mController),
+ OperationalDatasetTimestamp.fromInstant(Instant.now()),
+ Duration.ofSeconds(30));
+ CompletableFuture<Void> migrateFuture = new CompletableFuture<>();
+
+ mController.scheduleMigration(pendingDataset, mExecutor, newOutcomeReceiver(migrateFuture));
+
+ ThreadNetworkException thrown =
+ (ThreadNetworkException)
+ assertThrows(ExecutionException.class, migrateFuture::get).getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
+ }
+
+ @Test
+ public void scheduleMigration_secondRequestHasSmallerTimestamp_rejectedByLeader()
+ throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+ final ActiveOperationalDataset activeDataset =
+ new ActiveOperationalDataset.Builder(newRandomizedDataset("testNet", mController))
+ .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+ .build();
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+ .setNetworkName("testNet1")
+ .build();
+ PendingOperationalDataset pendingDataset1 =
+ new PendingOperationalDataset(
+ activeDataset1,
+ new OperationalDatasetTimestamp(100, 0, false),
+ Duration.ofSeconds(30));
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
+ .setNetworkName("testNet2")
+ .build();
+ PendingOperationalDataset pendingDataset2 =
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(20, 0, false),
+ Duration.ofSeconds(30));
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+ CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>();
+ mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+ mController.scheduleMigration(
+ pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
+ migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ mController.scheduleMigration(
+ pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
+
+ ThreadNetworkException thrown =
+ (ThreadNetworkException)
+ assertThrows(ExecutionException.class, migrateFuture2::get).getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_REJECTED_BY_PEER);
+ }
+
+ @Test
+ public void scheduleMigration_secondRequestHasLargerTimestamp_newDatasetApplied()
+ throws Exception {
+ grantPermissions(ACCESS_NETWORK_STATE, THREAD_NETWORK_PRIVILEGED);
+ final ActiveOperationalDataset activeDataset =
+ new ActiveOperationalDataset.Builder(newRandomizedDataset("validName", mController))
+ .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+ .build();
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+ .setNetworkName("testNet1")
+ .build();
+ PendingOperationalDataset pendingDataset1 =
+ new PendingOperationalDataset(
+ activeDataset1,
+ new OperationalDatasetTimestamp(100, 0, false),
+ Duration.ofSeconds(30));
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
+ .setNetworkName("testNet2")
+ .build();
+ PendingOperationalDataset pendingDataset2 =
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(200, 0, false),
+ Duration.ofSeconds(30));
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+ CompletableFuture<Void> migrateFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> migrateFuture2 = new CompletableFuture<>();
+ mController.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+
+ mController.scheduleMigration(
+ pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
+ migrateFuture1.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ mController.scheduleMigration(
+ pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
+ migrateFuture2.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+
+ CompletableFuture<Boolean> dataset2IsApplied = new CompletableFuture<>();
+ CompletableFuture<Boolean> pendingDatasetIsRemoved = new CompletableFuture<>();
+ OperationalDatasetCallback datasetCallback =
+ new OperationalDatasetCallback() {
+ @Override
+ public void onActiveOperationalDatasetChanged(
+ ActiveOperationalDataset activeDataset) {
+ if (activeDataset.equals(activeDataset2)) {
+ dataset2IsApplied.complete(true);
+ }
+ }
+
+ @Override
+ public void onPendingOperationalDatasetChanged(
+ PendingOperationalDataset pendingDataset) {
+ if (pendingDataset == null) {
+ pendingDatasetIsRemoved.complete(true);
+ }
+ }
+ };
+ mController.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+ try {
+ assertThat(dataset2IsApplied.get(MIGRATION_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+ assertThat(pendingDatasetIsRemoved.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isTrue();
+ } finally {
+ mController.unregisterOperationalDatasetCallback(datasetCallback);
+ }
+ }
+
+ @Test
+ public void scheduleMigration_threadDisabled_failsWithErrorThreadDisabled() throws Exception {
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", mController);
+ PendingOperationalDataset pendingDataset =
+ new PendingOperationalDataset(
+ activeDataset,
+ OperationalDatasetTimestamp.fromInstant(Instant.now()),
+ Duration.ofSeconds(30));
+ joinRandomizedDatasetAndWait(mController);
+ CompletableFuture<Void> migrationFuture = new CompletableFuture<>();
+
+ setEnabledAndWait(mController, false);
+
+ scheduleMigration(mController, pendingDataset, newOutcomeReceiver(migrationFuture));
+
+ ThreadNetworkException thrown =
+ (ThreadNetworkException)
+ assertThrows(ExecutionException.class, migrationFuture::get).getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
+ }
+
+ @Test
+ public void createRandomizedDataset_wrongNetworkNameLength_throwsIllegalArgumentException() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.createRandomizedDataset("", mExecutor, dataset -> {}));
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ mController.createRandomizedDataset(
+ "ANetNameIs17Bytes", mExecutor, dataset -> {}));
+ }
+
+ @Test
+ public void createRandomizedDataset_validNetworkName_success() throws Exception {
+ ActiveOperationalDataset dataset = newRandomizedDataset("validName", mController);
+
+ assertThat(dataset.getNetworkName()).isEqualTo("validName");
+ assertThat(dataset.getPanId()).isLessThan(0xffff);
+ assertThat(dataset.getChannelMask().size()).isAtLeast(1);
+ assertThat(dataset.getExtendedPanId()).hasLength(8);
+ assertThat(dataset.getNetworkKey()).hasLength(16);
+ assertThat(dataset.getPskc()).hasLength(16);
+ assertThat(dataset.getMeshLocalPrefix().getPrefixLength()).isEqualTo(64);
+ assertThat(dataset.getMeshLocalPrefix().getRawAddress()[0]).isEqualTo((byte) 0xfd);
+ }
+
+ @Test
+ public void setEnabled_permissionsGranted_succeeds() throws Exception {
+ CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture1)));
+ setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+ waitForEnabledState(mController, booleanToEnabledState(false));
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> mController.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture2)));
+ setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+ waitForEnabledState(mController, booleanToEnabledState(true));
+ }
+
+ @Test
+ public void setEnabled_noPermissions_throwsSecurityException() throws Exception {
+ CompletableFuture<Void> setFuture = new CompletableFuture<>();
+ assertThrows(
+ SecurityException.class,
+ () -> mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture)));
+ }
+
+ @Test
+ public void setEnabled_disable_leavesThreadNetwork() throws Exception {
+ joinRandomizedDatasetAndWait(mController);
+ setEnabledAndWait(mController, false);
+ assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+ }
+
+ @Test
+ public void setEnabled_enableFollowedByDisable_allSucceed() throws Exception {
+ joinRandomizedDatasetAndWait(mController);
+ CompletableFuture<Void> setFuture1 = new CompletableFuture<>();
+ CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
+ EnabledStateListener listener = new EnabledStateListener(mController);
+ listener.expectThreadEnabledState(STATE_ENABLED);
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mController.setEnabled(true, mExecutor, newOutcomeReceiver(setFuture1));
+ mController.setEnabled(false, mExecutor, newOutcomeReceiver(setFuture2));
+ });
+ setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+ setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+
+ listener.expectThreadEnabledState(STATE_DISABLING);
+ listener.expectThreadEnabledState(STATE_DISABLED);
+ assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+ // FIXME: this is not called when a exception is thrown after the creation of `listener`
+ listener.unregisterStateCallback();
+ }
+
+ // TODO (b/322437869): add test case to verify when Thread is in DISABLING state, any commands
+ // (join/leave/scheduleMigration/setEnabled) fail with ERROR_BUSY. This is not currently tested
+ // because DISABLING has very short lifecycle, it's not possible to guarantee the command can be
+ // sent before state changes to DISABLED.
+
+ @Test
+ public void threadNetworkCallback_deviceAttached_threadNetworkIsAvailable() throws Exception {
+ CompletableFuture<Network> networkFuture = new CompletableFuture<>();
+ 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.complete(network);
+ }
+ };
+
+ joinRandomizedDatasetAndWait(mController);
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> cm.registerNetworkCallback(networkRequest, networkCallback));
+
+ assertThat(isAttached(mController)).isTrue();
+ assertThat(networkFuture.get(NETWORK_CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNotNull();
+ NetworkCapabilities caps =
+ runAsShell(
+ ACCESS_NETWORK_STATE, () -> cm.getNetworkCapabilities(networkFuture.get()));
+ assertThat(caps).isNotNull();
+ assertThat(caps.hasTransport(NetworkCapabilities.TRANSPORT_THREAD)).isTrue();
+ assertThat(caps.getCapabilities())
+ .asList()
+ .containsAtLeast(
+ NET_CAPABILITY_LOCAL_NETWORK,
+ NET_CAPABILITY_NOT_METERED,
+ NET_CAPABILITY_NOT_RESTRICTED,
+ NET_CAPABILITY_NOT_VCN_MANAGED,
+ NET_CAPABILITY_NOT_VPN,
+ NET_CAPABILITY_TRUSTED);
+ }
+
private void grantPermissions(String... permissions) {
for (String permission : permissions) {
mGrantedPermissions.add(permission);
@@ -128,14 +826,82 @@
getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(allPermissions);
}
+ @Test
+ public void meshcopService_threadEnabledButNotJoined_discoveredButNoNetwork() throws Exception {
+ setUpTestNetwork();
+
+ setEnabledAndWait(mController, true);
+ leaveAndWait(mController);
+
+ NsdServiceInfo serviceInfo =
+ expectServiceResolved(
+ MESHCOP_SERVICE_TYPE,
+ SERVICE_DISCOVERY_TIMEOUT_MILLIS,
+ s -> s.getAttributes().get("at") == null);
+
+ Map<String, byte[]> txtMap = serviceInfo.getAttributes();
+
+ assertThat(txtMap.get("rv")).isNotNull();
+ assertThat(txtMap.get("tv")).isNotNull();
+ assertThat(txtMap.get("sb")).isNotNull();
+ }
+
+ @Test
+ @Ignore("b/333649897: Enable this when it's not flaky at all")
+ public void meshcopService_joinedNetwork_discoveredHasNetwork() throws Exception {
+ setUpTestNetwork();
+
+ String networkName = "TestNet" + new Random().nextInt(10_000);
+ joinRandomizedDatasetAndWait(mController, networkName);
+
+ Predicate<NsdServiceInfo> predicate =
+ serviceInfo ->
+ serviceInfo.getAttributes().get("at") != null
+ && Arrays.equals(
+ serviceInfo.getAttributes().get("nn"),
+ networkName.getBytes(StandardCharsets.UTF_8));
+
+ NsdServiceInfo resolvedService =
+ expectServiceResolved(
+ MESHCOP_SERVICE_TYPE, SERVICE_DISCOVERY_TIMEOUT_MILLIS, predicate);
+
+ Map<String, byte[]> txtMap = resolvedService.getAttributes();
+ assertThat(txtMap.get("rv")).isNotNull();
+ assertThat(txtMap.get("tv")).isNotNull();
+ assertThat(txtMap.get("sb")).isNotNull();
+ assertThat(txtMap.get("id").length).isEqualTo(16);
+ }
+
+ @Test
+ public void meshcopService_threadDisabled_notDiscovered() throws Exception {
+ setUpTestNetwork();
+ CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
+ NsdManager.DiscoveryListener listener =
+ discoverForServiceLost(MESHCOP_SERVICE_TYPE, serviceLostFuture);
+
+ setEnabledAndWait(mController, false);
+
+ try {
+ serviceLostFuture.get(SERVICE_LOST_TIMEOUT_MILLIS, MILLISECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException ignored) {
+ // It's fine if the service lost event didn't show up. The service may not ever be
+ // advertised.
+ } finally {
+ mNsdManager.stopServiceDiscovery(listener);
+ }
+ assertThrows(
+ TimeoutException.class,
+ () -> discoverService(MESHCOP_SERVICE_TYPE, SERVICE_LOST_TIMEOUT_MILLIS));
+ }
+
private static void dropAllPermissions() {
getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
}
private static ActiveOperationalDataset newRandomizedDataset(
String networkName, ThreadNetworkController controller) throws Exception {
- SettableFuture<ActiveOperationalDataset> future = SettableFuture.create();
- controller.createRandomizedDataset(networkName, directExecutor(), future::set);
+ CompletableFuture<ActiveOperationalDataset> future = new CompletableFuture<>();
+ controller.createRandomizedDataset(networkName, directExecutor(), future::complete);
return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
}
@@ -144,610 +910,318 @@
}
private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
- SettableFuture<Integer> future = SettableFuture.create();
- StateCallback callback = future::set;
- controller.registerStateCallback(directExecutor(), callback);
- int role = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
- controller.unregisterStateCallback(callback);
- return role;
+ CompletableFuture<Integer> future = new CompletableFuture<>();
+ StateCallback callback = future::complete;
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> controller.registerStateCallback(directExecutor(), callback));
+ try {
+ return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> controller.unregisterStateCallback(callback));
+ }
+ }
+
+ private static int waitForAttachedState(ThreadNetworkController controller) throws Exception {
+ List<Integer> attachedRoles = new ArrayList<>();
+ attachedRoles.add(DEVICE_ROLE_CHILD);
+ attachedRoles.add(DEVICE_ROLE_ROUTER);
+ attachedRoles.add(DEVICE_ROLE_LEADER);
+ return waitForStateAnyOf(controller, attachedRoles);
}
private static int waitForStateAnyOf(
ThreadNetworkController controller, List<Integer> deviceRoles) throws Exception {
- SettableFuture<Integer> future = SettableFuture.create();
+ CompletableFuture<Integer> future = new CompletableFuture<>();
StateCallback callback =
newRole -> {
if (deviceRoles.contains(newRole)) {
- future.set(newRole);
+ future.complete(newRole);
}
};
controller.registerStateCallback(directExecutor(), callback);
- int role = future.get();
+ int role = future.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
controller.unregisterStateCallback(callback);
return role;
}
+ private static void waitForEnabledState(ThreadNetworkController controller, int state)
+ throws Exception {
+ CompletableFuture<Integer> future = new CompletableFuture<>();
+ StateCallback callback =
+ new ThreadNetworkController.StateCallback() {
+ @Override
+ public void onDeviceRoleChanged(int r) {}
+
+ @Override
+ public void onThreadEnableStateChanged(int enabled) {
+ if (enabled == state) {
+ future.complete(enabled);
+ }
+ }
+ };
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> controller.registerStateCallback(directExecutor(), callback));
+ future.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+ runAsShell(ACCESS_NETWORK_STATE, () -> controller.unregisterStateCallback(callback));
+ }
+
+ private void leave(
+ ThreadNetworkController controller,
+ OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ runAsShell(THREAD_NETWORK_PRIVILEGED, () -> controller.leave(mExecutor, receiver));
+ }
+
+ private void leaveAndWait(ThreadNetworkController controller) throws Exception {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ leave(controller, future::complete);
+ future.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+ }
+
+ private void scheduleMigration(
+ ThreadNetworkController controller,
+ PendingOperationalDataset pendingDataset,
+ OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> controller.scheduleMigration(pendingDataset, mExecutor, receiver));
+ }
+
+ private class EnabledStateListener {
+ private ArrayTrackRecord<Integer> mEnabledStates = new ArrayTrackRecord<>();
+ private final ArrayTrackRecord<Integer>.ReadHead mReadHead = mEnabledStates.newReadHead();
+ ThreadNetworkController mController;
+ StateCallback mCallback =
+ new ThreadNetworkController.StateCallback() {
+ @Override
+ public void onDeviceRoleChanged(int r) {}
+
+ @Override
+ public void onThreadEnableStateChanged(int enabled) {
+ mEnabledStates.add(enabled);
+ }
+ };
+
+ EnabledStateListener(ThreadNetworkController controller) {
+ this.mController = controller;
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> controller.registerStateCallback(mExecutor, mCallback));
+ }
+
+ public void expectThreadEnabledState(int enabled) {
+ assertNotNull(mReadHead.poll(ENABLED_TIMEOUT_MILLIS, e -> (e == enabled)));
+ }
+
+ public void unregisterStateCallback() {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(mCallback));
+ }
+ }
+
+ private int booleanToEnabledState(boolean enabled) {
+ return enabled ? STATE_ENABLED : STATE_DISABLED;
+ }
+
+ private void setEnabledAndWait(ThreadNetworkController controller, boolean enabled)
+ throws Exception {
+ CompletableFuture<Void> setFuture = new CompletableFuture<>();
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> controller.setEnabled(enabled, mExecutor, newOutcomeReceiver(setFuture)));
+ setFuture.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
+ waitForEnabledState(controller, booleanToEnabledState(enabled));
+ }
+
+ private CompletableFuture joinRandomizedDataset(
+ ThreadNetworkController controller, String networkName) throws Exception {
+ ActiveOperationalDataset activeDataset = newRandomizedDataset(networkName, controller);
+ CompletableFuture<Void> joinFuture = new CompletableFuture<>();
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () -> controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture)));
+ return joinFuture;
+ }
+
+ private void joinRandomizedDatasetAndWait(ThreadNetworkController controller) throws Exception {
+ joinRandomizedDatasetAndWait(controller, "TestNet");
+ }
+
+ private void joinRandomizedDatasetAndWait(
+ ThreadNetworkController controller, String networkName) throws Exception {
+ CompletableFuture<Void> joinFuture = joinRandomizedDataset(controller, networkName);
+ joinFuture.get(JOIN_TIMEOUT_MILLIS, MILLISECONDS);
+ assertThat(isAttached(controller)).isTrue();
+ }
+
private static ActiveOperationalDataset getActiveOperationalDataset(
ThreadNetworkController controller) throws Exception {
- SettableFuture<ActiveOperationalDataset> future = SettableFuture.create();
- OperationalDatasetCallback callback = future::set;
- controller.registerOperationalDatasetCallback(directExecutor(), callback);
- ActiveOperationalDataset dataset = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
- controller.unregisterOperationalDatasetCallback(callback);
- return dataset;
+ CompletableFuture<ActiveOperationalDataset> future = new CompletableFuture<>();
+ OperationalDatasetCallback callback = future::complete;
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ THREAD_NETWORK_PRIVILEGED,
+ () -> controller.registerOperationalDatasetCallback(directExecutor(), callback));
+ try {
+ return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ } finally {
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ THREAD_NETWORK_PRIVILEGED,
+ () -> controller.unregisterOperationalDatasetCallback(callback));
+ }
}
private static PendingOperationalDataset getPendingOperationalDataset(
ThreadNetworkController controller) throws Exception {
- SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
- SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ CompletableFuture<ActiveOperationalDataset> activeFuture = new CompletableFuture<>();
+ CompletableFuture<PendingOperationalDataset> pendingFuture = new CompletableFuture<>();
controller.registerOperationalDatasetCallback(
directExecutor(), newDatasetCallback(activeFuture, pendingFuture));
- return pendingFuture.get();
+ return pendingFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
}
private static OperationalDatasetCallback newDatasetCallback(
- SettableFuture<ActiveOperationalDataset> activeFuture,
- SettableFuture<PendingOperationalDataset> pendingFuture) {
+ CompletableFuture<ActiveOperationalDataset> activeFuture,
+ CompletableFuture<PendingOperationalDataset> pendingFuture) {
return new OperationalDatasetCallback() {
@Override
public void onActiveOperationalDatasetChanged(
ActiveOperationalDataset activeOpDataset) {
- activeFuture.set(activeOpDataset);
+ activeFuture.complete(activeOpDataset);
}
@Override
public void onPendingOperationalDatasetChanged(
PendingOperationalDataset pendingOpDataset) {
- pendingFuture.set(pendingOpDataset);
+ pendingFuture.complete(pendingOpDataset);
}
};
}
- @Test
- public void getThreadVersion_returnsAtLeastThreadVersion1P3() {
- for (ThreadNetworkController controller : getAllControllers()) {
- assertThat(controller.getThreadVersion()).isAtLeast(THREAD_VERSION_1_3);
+ private static void assertDoesNotThrow(ThrowingRunnable runnable) {
+ try {
+ runnable.run();
+ } catch (Throwable e) {
+ fail("Should not have thrown " + e);
}
}
- @Test
- public void registerStateCallback_permissionsGranted_returnsCurrentStates() throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<Integer> deviceRole = SettableFuture.create();
- StateCallback callback = deviceRole::set;
-
- try {
- controller.registerStateCallback(mExecutor, callback);
-
- assertThat(deviceRole.get()).isEqualTo(DEVICE_ROLE_STOPPED);
- } finally {
- controller.unregisterStateCallback(callback);
- }
- }
+ // Return the first discovered service instance.
+ private NsdServiceInfo discoverService(String serviceType) throws Exception {
+ return discoverService(serviceType, SERVICE_DISCOVERY_TIMEOUT_MILLIS);
}
- @Test
- public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception {
- dropAllPermissions();
-
- for (ThreadNetworkController controller : getAllControllers()) {
- assertThrows(
- SecurityException.class,
- () -> controller.registerStateCallback(mExecutor, role -> {}));
- }
- }
-
- @Test
- public void registerStateCallback_alreadyRegistered_throwsIllegalArgumentException()
+ // Return the first discovered service instance.
+ private NsdServiceInfo discoverService(String serviceType, int timeoutMilliseconds)
throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<Integer> deviceRole = SettableFuture.create();
- StateCallback callback = role -> deviceRole.set(role);
- controller.registerStateCallback(mExecutor, callback);
-
- assertThrows(
- IllegalArgumentException.class,
- () -> controller.registerStateCallback(mExecutor, callback));
+ CompletableFuture<NsdServiceInfo> serviceInfoFuture = new CompletableFuture<>();
+ NsdManager.DiscoveryListener listener =
+ new DefaultDiscoveryListener() {
+ @Override
+ public void onServiceFound(NsdServiceInfo serviceInfo) {
+ serviceInfoFuture.complete(serviceInfo);
+ }
+ };
+ mNsdManager.discoverServices(
+ serviceType,
+ NsdManager.PROTOCOL_DNS_SD,
+ mTestNetworkTracker.getNetwork(),
+ mExecutor,
+ listener);
+ try {
+ serviceInfoFuture.get(timeoutMilliseconds, MILLISECONDS);
+ } finally {
+ mNsdManager.stopServiceDiscovery(listener);
}
+
+ return serviceInfoFuture.get();
}
- @Test
- public void unregisterStateCallback_noPermissions_throwsSecurityException() throws Exception {
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<Integer> deviceRole = SettableFuture.create();
- StateCallback callback = role -> deviceRole.set(role);
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- controller.registerStateCallback(mExecutor, callback);
-
- try {
- dropAllPermissions();
- assertThrows(
- SecurityException.class,
- () -> controller.unregisterStateCallback(callback));
- } finally {
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- controller.unregisterStateCallback(callback);
- }
- }
+ private NsdManager.DiscoveryListener discoverForServiceLost(
+ String serviceType, CompletableFuture<NsdServiceInfo> serviceInfoFuture) {
+ NsdManager.DiscoveryListener listener =
+ new DefaultDiscoveryListener() {
+ @Override
+ public void onServiceLost(NsdServiceInfo serviceInfo) {
+ serviceInfoFuture.complete(serviceInfo);
+ }
+ };
+ mNsdManager.discoverServices(
+ serviceType,
+ NsdManager.PROTOCOL_DNS_SD,
+ mTestNetworkTracker.getNetwork(),
+ mExecutor,
+ listener);
+ return listener;
}
- @Test
- public void unregisterStateCallback_callbackRegistered_success() throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<Integer> deviceRole = SettableFuture.create();
- StateCallback callback = role -> deviceRole.set(role);
- controller.registerStateCallback(mExecutor, callback);
-
- controller.unregisterStateCallback(callback);
- }
- }
-
- @Test
- public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException()
+ private NsdServiceInfo expectServiceResolved(
+ String serviceType, int timeoutMilliseconds, Predicate<NsdServiceInfo> predicate)
throws Exception {
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<Integer> deviceRole = SettableFuture.create();
- StateCallback callback = role -> deviceRole.set(role);
-
- assertThrows(
- IllegalArgumentException.class,
- () -> controller.unregisterStateCallback(callback));
- }
- }
-
- @Test
- public void unregisterStateCallback_alreadyUnregistered_throwsIllegalArgumentException()
- throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<Integer> deviceRole = SettableFuture.create();
- StateCallback callback = deviceRole::set;
- controller.registerStateCallback(mExecutor, callback);
- controller.unregisterStateCallback(callback);
-
- assertThrows(
- IllegalArgumentException.class,
- () -> controller.unregisterStateCallback(callback));
- }
- }
-
- @Test
- public void registerOperationalDatasetCallback_permissionsGranted_returnsCurrentStates()
- throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
- SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
- var callback = newDatasetCallback(activeFuture, pendingFuture);
-
- try {
- controller.registerOperationalDatasetCallback(mExecutor, callback);
-
- assertThat(activeFuture.get()).isNull();
- assertThat(pendingFuture.get()).isNull();
- } finally {
- controller.unregisterOperationalDatasetCallback(callback);
- }
- }
- }
-
- @Test
- public void registerOperationalDatasetCallback_noPermissions_throwsSecurityException()
- throws Exception {
- dropAllPermissions();
-
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
- SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
- var callback = newDatasetCallback(activeFuture, pendingFuture);
-
- assertThrows(
- SecurityException.class,
- () -> controller.registerOperationalDatasetCallback(mExecutor, callback));
- }
- }
-
- @Test
- public void unregisterOperationalDatasetCallback_callbackRegistered_success() throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
- SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
- var callback = newDatasetCallback(activeFuture, pendingFuture);
- controller.registerOperationalDatasetCallback(mExecutor, callback);
-
- controller.unregisterOperationalDatasetCallback(callback);
- }
- }
-
- @Test
- public void unregisterOperationalDatasetCallback_noPermissions_throwsSecurityException()
- throws Exception {
- dropAllPermissions();
-
- for (ThreadNetworkController controller : getAllControllers()) {
- SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
- SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
- var callback = newDatasetCallback(activeFuture, pendingFuture);
- grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
- controller.registerOperationalDatasetCallback(mExecutor, callback);
-
- try {
- dropAllPermissions();
- assertThrows(
- SecurityException.class,
- () -> controller.unregisterOperationalDatasetCallback(callback));
- } finally {
- grantPermissions(
- permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
- controller.unregisterOperationalDatasetCallback(callback);
- }
- }
- }
-
- private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
- SettableFuture<V> future) {
- return new OutcomeReceiver<V, ThreadNetworkException>() {
- @Override
- public void onResult(V result) {
- future.set(result);
- }
-
- @Override
- public void onError(ThreadNetworkException e) {
- future.setException(e);
- }
- };
- }
-
- @Test
- public void join_withPrivilegedPermission_success() throws Exception {
- grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
- SettableFuture<Void> joinFuture = SettableFuture.create();
-
- controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
- joinFuture.get();
-
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- assertThat(isAttached(controller)).isTrue();
- assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset);
- }
- }
-
- @Test
- public void join_withoutPrivilegedPermission_throwsSecurityException() throws Exception {
- dropAllPermissions();
-
- for (ThreadNetworkController controller : getAllControllers()) {
- ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
-
- assertThrows(
- SecurityException.class,
- () -> controller.join(activeDataset, mExecutor, v -> {}));
- }
- }
-
- @Test
- public void join_concurrentRequests_firstOneIsAborted() throws Exception {
- grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- final byte[] KEY_1 = new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
- final byte[] KEY_2 = new byte[] {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2};
- for (ThreadNetworkController controller : getAllControllers()) {
- ActiveOperationalDataset activeDataset1 =
- new ActiveOperationalDataset.Builder(
- newRandomizedDataset("TestNet", controller))
- .setNetworkKey(KEY_1)
- .build();
- ActiveOperationalDataset activeDataset2 =
- new ActiveOperationalDataset.Builder(activeDataset1)
- .setNetworkKey(KEY_2)
- .build();
- SettableFuture<Void> joinFuture1 = SettableFuture.create();
- SettableFuture<Void> joinFuture2 = SettableFuture.create();
-
- controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture1));
- controller.join(activeDataset2, mExecutor, newOutcomeReceiver(joinFuture2));
-
- ThreadNetworkException thrown =
- (ThreadNetworkException)
- assertThrows(ExecutionException.class, joinFuture1::get).getCause();
- assertThat(thrown.getErrorCode()).isEqualTo(ERROR_ABORTED);
- joinFuture2.get();
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- assertThat(isAttached(controller)).isTrue();
- assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
- }
- }
-
- @Test
- public void leave_withPrivilegedPermission_success() throws Exception {
- grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
- SettableFuture<Void> joinFuture = SettableFuture.create();
- SettableFuture<Void> leaveFuture = SettableFuture.create();
- controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
- joinFuture.get();
-
- controller.leave(mExecutor, newOutcomeReceiver(leaveFuture));
- leaveFuture.get();
-
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
- }
- }
-
- @Test
- public void leave_withoutPrivilegedPermission_throwsSecurityException() {
- dropAllPermissions();
-
- for (ThreadNetworkController controller : getAllControllers()) {
- assertThrows(SecurityException.class, () -> controller.leave(mExecutor, v -> {}));
- }
- }
-
- @Test
- public void leave_concurrentRequests_bothSuccess() throws Exception {
- grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
- SettableFuture<Void> joinFuture = SettableFuture.create();
- SettableFuture<Void> leaveFuture1 = SettableFuture.create();
- SettableFuture<Void> leaveFuture2 = SettableFuture.create();
- controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
- joinFuture.get();
-
- controller.leave(mExecutor, newOutcomeReceiver(leaveFuture1));
- controller.leave(mExecutor, newOutcomeReceiver(leaveFuture2));
-
- leaveFuture1.get();
- leaveFuture2.get();
- grantPermissions(permission.ACCESS_NETWORK_STATE);
- assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
- }
- }
-
- @Test
- public void scheduleMigration_withPrivilegedPermission_newDatasetApplied() throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- ActiveOperationalDataset activeDataset1 =
- new ActiveOperationalDataset.Builder(
- newRandomizedDataset("TestNet", controller))
- .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
- .setExtendedPanId(new byte[] {1, 1, 1, 1, 1, 1, 1, 1})
- .build();
- ActiveOperationalDataset activeDataset2 =
- new ActiveOperationalDataset.Builder(activeDataset1)
- .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
- .setNetworkName("ThreadNet2")
- .build();
- PendingOperationalDataset pendingDataset2 =
- new PendingOperationalDataset(
- activeDataset2,
- OperationalDatasetTimestamp.fromInstant(Instant.now()),
- Duration.ofSeconds(30));
- SettableFuture<Void> joinFuture = SettableFuture.create();
- SettableFuture<Void> migrateFuture = SettableFuture.create();
- controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture));
- joinFuture.get();
-
- controller.scheduleMigration(
- pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture));
- migrateFuture.get();
-
- 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);
- }
+ NsdServiceInfo discoveredServiceInfo = discoverService(serviceType);
+ CompletableFuture<NsdServiceInfo> future = new CompletableFuture<>();
+ NsdManager.ServiceInfoCallback callback =
+ new DefaultServiceInfoCallback() {
+ @Override
+ public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
+ if (predicate.test(serviceInfo)) {
+ future.complete(serviceInfo);
}
-
- @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);
+ }
+ };
+ mNsdManager.registerServiceInfoCallback(discoveredServiceInfo, mExecutor, callback);
+ try {
+ return future.get(timeoutMilliseconds, MILLISECONDS);
+ } finally {
+ mNsdManager.unregisterServiceInfoCallback(callback);
}
}
- @Test
- public void scheduleMigration_whenNotAttached_failWithPreconditionError() throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- PendingOperationalDataset pendingDataset =
- new PendingOperationalDataset(
- newRandomizedDataset("TestNet", controller),
- OperationalDatasetTimestamp.fromInstant(Instant.now()),
- Duration.ofSeconds(30));
- SettableFuture<Void> migrateFuture = SettableFuture.create();
-
- controller.scheduleMigration(
- pendingDataset, mExecutor, newOutcomeReceiver(migrateFuture));
-
- ThreadNetworkException thrown =
- (ThreadNetworkException)
- assertThrows(ExecutionException.class, migrateFuture::get).getCause();
- assertThat(thrown.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
- }
+ private void setUpTestNetwork() {
+ assertThat(mTestNetworkTracker).isNull();
+ mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper());
}
- @Test
- public void scheduleMigration_secondRequestHasSmallerTimestamp_rejectedByLeader()
- throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
-
- for (ThreadNetworkController controller : getAllControllers()) {
- final ActiveOperationalDataset activeDataset =
- new ActiveOperationalDataset.Builder(
- newRandomizedDataset("testNet", controller))
- .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
- .build();
- ActiveOperationalDataset activeDataset1 =
- new ActiveOperationalDataset.Builder(activeDataset)
- .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
- .setNetworkName("testNet1")
- .build();
- PendingOperationalDataset pendingDataset1 =
- new PendingOperationalDataset(
- activeDataset1,
- new OperationalDatasetTimestamp(100, 0, false),
- Duration.ofSeconds(30));
- ActiveOperationalDataset activeDataset2 =
- new ActiveOperationalDataset.Builder(activeDataset)
- .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
- .setNetworkName("testNet2")
- .build();
- PendingOperationalDataset pendingDataset2 =
- new PendingOperationalDataset(
- activeDataset2,
- new OperationalDatasetTimestamp(20, 0, false),
- Duration.ofSeconds(30));
- SettableFuture<Void> joinFuture = SettableFuture.create();
- SettableFuture<Void> migrateFuture1 = SettableFuture.create();
- SettableFuture<Void> migrateFuture2 = SettableFuture.create();
- controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
- joinFuture.get();
-
- controller.scheduleMigration(
- pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
- migrateFuture1.get();
- controller.scheduleMigration(
- pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
-
- ThreadNetworkException thrown =
- (ThreadNetworkException)
- assertThrows(ExecutionException.class, migrateFuture2::get).getCause();
- assertThat(thrown.getErrorCode()).isEqualTo(ERROR_REJECTED_BY_PEER);
+ private void tearDownTestNetwork() throws InterruptedException {
+ if (mTestNetworkTracker != null) {
+ mTestNetworkTracker.tearDown();
}
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
}
- @Test
- public void scheduleMigration_secondRequestHasLargerTimestamp_newDatasetApplied()
- throws Exception {
- grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener {
+ @Override
+ public void onStartDiscoveryFailed(String serviceType, int errorCode) {}
- for (ThreadNetworkController controller : getAllControllers()) {
- final ActiveOperationalDataset activeDataset =
- new ActiveOperationalDataset.Builder(
- newRandomizedDataset("validName", controller))
- .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
- .build();
- ActiveOperationalDataset activeDataset1 =
- new ActiveOperationalDataset.Builder(activeDataset)
- .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
- .setNetworkName("testNet1")
- .build();
- PendingOperationalDataset pendingDataset1 =
- new PendingOperationalDataset(
- activeDataset1,
- new OperationalDatasetTimestamp(100, 0, false),
- Duration.ofSeconds(30));
- ActiveOperationalDataset activeDataset2 =
- new ActiveOperationalDataset.Builder(activeDataset)
- .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
- .setNetworkName("testNet2")
- .build();
- PendingOperationalDataset pendingDataset2 =
- new PendingOperationalDataset(
- activeDataset2,
- new OperationalDatasetTimestamp(200, 0, false),
- Duration.ofSeconds(30));
- SettableFuture<Void> joinFuture = SettableFuture.create();
- SettableFuture<Void> migrateFuture1 = SettableFuture.create();
- SettableFuture<Void> migrateFuture2 = SettableFuture.create();
- controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
- joinFuture.get();
+ @Override
+ public void onStopDiscoveryFailed(String serviceType, int errorCode) {}
- controller.scheduleMigration(
- pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
- migrateFuture1.get();
- controller.scheduleMigration(
- pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
- migrateFuture2.get();
+ @Override
+ public void onDiscoveryStarted(String serviceType) {}
- 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 onDiscoveryStopped(String serviceType) {}
- @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);
- }
+ @Override
+ public void onServiceFound(NsdServiceInfo serviceInfo) {}
+
+ @Override
+ public void onServiceLost(NsdServiceInfo serviceInfo) {}
}
- @Test
- public void createRandomizedDataset_wrongNetworkNameLength_throwsIllegalArgumentException() {
- for (ThreadNetworkController controller : getAllControllers()) {
- assertThrows(
- IllegalArgumentException.class,
- () -> controller.createRandomizedDataset("", mExecutor, dataset -> {}));
+ private static class DefaultServiceInfoCallback implements NsdManager.ServiceInfoCallback {
+ @Override
+ public void onServiceInfoCallbackRegistrationFailed(int errorCode) {}
- assertThrows(
- IllegalArgumentException.class,
- () ->
- controller.createRandomizedDataset(
- "ANetNameIs17Bytes", mExecutor, dataset -> {}));
- }
- }
+ @Override
+ public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {}
- @Test
- public void createRandomizedDataset_validNetworkName_success() throws Exception {
- for (ThreadNetworkController controller : getAllControllers()) {
- ActiveOperationalDataset dataset = newRandomizedDataset("validName", controller);
+ @Override
+ public void onServiceLost() {}
- assertThat(dataset.getNetworkName()).isEqualTo("validName");
- assertThat(dataset.getPanId()).isLessThan(0xffff);
- assertThat(dataset.getChannelMask().size()).isAtLeast(1);
- assertThat(dataset.getExtendedPanId()).hasLength(8);
- assertThat(dataset.getNetworkKey()).hasLength(16);
- assertThat(dataset.getPskc()).hasLength(16);
- assertThat(dataset.getMeshLocalPrefix().getPrefixLength()).isEqualTo(64);
- assertThat(dataset.getMeshLocalPrefix().getRawAddress()[0]).isEqualTo((byte) 0xfd);
- }
+ @Override
+ public void onServiceInfoCallbackUnregistered() {}
}
}
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java
new file mode 100644
index 0000000..7d9ae81
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkExceptionTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread.cts;
+
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
+import static android.net.thread.ThreadNetworkException.ERROR_UNKNOWN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.thread.ThreadNetworkException;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** CTS tests for {@link ThreadNetworkException}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ThreadNetworkExceptionTest {
+ @Test
+ public void constructor_validValues_valuesAreConnectlySet() throws Exception {
+ ThreadNetworkException errorThreadDisabled =
+ new ThreadNetworkException(ERROR_THREAD_DISABLED, "Thread disabled error!");
+ ThreadNetworkException errorInternalError =
+ new ThreadNetworkException(ERROR_INTERNAL_ERROR, "internal error!");
+
+ assertThat(errorThreadDisabled.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
+ assertThat(errorThreadDisabled.getMessage()).isEqualTo("Thread disabled error!");
+ assertThat(errorInternalError.getErrorCode()).isEqualTo(ERROR_INTERNAL_ERROR);
+ assertThat(errorInternalError.getMessage()).isEqualTo("internal error!");
+ }
+
+ @Test
+ public void constructor_nullMessage_throwsNullPointerException() throws Exception {
+ assertThrows(
+ NullPointerException.class,
+ () -> new ThreadNetworkException(ERROR_UNKNOWN, null /* message */));
+ }
+
+ @Test
+ public void constructor_tooSmallErrorCode_throwsIllegalArgumentException() throws Exception {
+ assertThrows(IllegalArgumentException.class, () -> new ThreadNetworkException(0, "0"));
+ // TODO: add argument check for too large error code when mainline CTS is ready. This was
+ // not added here for CTS forward copatibility.
+ }
+}
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
index 405fb76..94985b1 100644
--- a/thread/tests/integration/Android.bp
+++ b/thread/tests/integration/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_thread_network",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -23,12 +24,16 @@
min_sdk_version: "30",
static_libs: [
"androidx.test.rules",
+ "compatibility-device-util-axt",
"guava",
"mockito-target-minus-junit4",
"net-tests-utils",
"net-utils-device-common",
"net-utils-device-common-bpf",
+ "net-utils-device-common-struct-base",
"testables",
+ "ThreadNetworkTestUtils",
+ "truth",
],
libs: [
"android.test.runner",
@@ -41,11 +46,13 @@
name: "ThreadNetworkIntegrationTests",
platform_apis: true,
manifest: "AndroidManifest.xml",
+ test_config: "AndroidTest.xml",
defaults: [
"framework-connectivity-test-defaults",
- "ThreadNetworkIntegrationTestsDefaults"
+ "ThreadNetworkIntegrationTestsDefaults",
],
test_suites: [
+ "mts-tethering",
"general-tests",
],
srcs: [
diff --git a/thread/tests/integration/AndroidManifest.xml b/thread/tests/integration/AndroidManifest.xml
index a347654..a049184 100644
--- a/thread/tests/integration/AndroidManifest.xml
+++ b/thread/tests/integration/AndroidManifest.xml
@@ -23,6 +23,7 @@
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">
diff --git a/thread/tests/integration/AndroidTest.xml b/thread/tests/integration/AndroidTest.xml
new file mode 100644
index 0000000..152c1c3
--- /dev/null
+++ b/thread/tests/integration/AndroidTest.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<configuration description="Config for Thread integration tests">
+ <option name="test-tag" value="ThreadNetworkIntegrationTests" />
+ <option name="test-suite-tag" value="apct" />
+
+ <!--
+ Only run tests if the device under test is SDK version 34 (Android 14) or above.
+ -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.Sdk34ModuleController" />
+
+ <!-- Run tests in MTS only if the Tethering Mainline module is installed. -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+
+ <!-- Install test -->
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="ThreadNetworkIntegrationTests.apk" />
+ <option name="check-min-sdk" value="true" />
+ <option name="cleanup-apks" value="true" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.thread.tests.integration" />
+ </test>
+</configuration>
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index 5d3818a..8c63d37 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -17,28 +17,49 @@
package android.net.thread;
import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
-import static android.net.thread.IntegrationTestUtils.isExpectedIcmpv6Packet;
-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 android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
+import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
+import static android.net.thread.utils.IntegrationTestUtils.isFromIpv6Source;
+import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
+import static android.net.thread.utils.IntegrationTestUtils.isToIpv6Destination;
+import static android.net.thread.utils.IntegrationTestUtils.newPacketReader;
+import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
+import static android.net.thread.utils.IntegrationTestUtils.sendUdpMessage;
+import static android.net.thread.utils.IntegrationTestUtils.waitFor;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import static java.util.Objects.requireNonNull;
import android.content.Context;
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.MacAddress;
+import android.net.thread.utils.FullThreadDevice;
+import android.net.thread.utils.InfraNetworkDevice;
+import android.net.thread.utils.OtDaemonController;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresIpv6MulticastRouting;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+import android.net.thread.utils.ThreadNetworkControllerWrapper;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.SystemClock;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
@@ -47,30 +68,32 @@
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.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.net.Inet6Address;
+import java.time.Duration;
+import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
/** Integration test cases for Thread Border Routing feature. */
@RunWith(AndroidJUnit4.class)
+@RequiresThreadFeature
+@RequiresSimulationThreadDevice
@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;
+ private static final int NUM_FTD = 2;
+ private static final Inet6Address GROUP_ADDR_SCOPE_5 =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+ private static final Inet6Address GROUP_ADDR_SCOPE_4 =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff04::1234");
+ private static final Inet6Address GROUP_ADDR_SCOPE_3 =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff03::1234");
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
private static final byte[] DEFAULT_DATASET_TLVS =
@@ -83,54 +106,61 @@
private static final ActiveOperationalDataset DEFAULT_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+ @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final ThreadNetworkControllerWrapper mController =
+ ThreadNetworkControllerWrapper.newInstance(mContext);
+ private OtDaemonController mOtCtl;
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+ private TestNetworkTracker mInfraNetworkTracker;
+ private List<FullThreadDevice> mFtds;
+ private TapPacketReader mInfraNetworkReader;
+ private InfraNetworkDevice mInfraDevice;
+
@Before
public void setUp() throws Exception {
+ // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
+ mOtCtl = new OtDaemonController();
+ mOtCtl.factoryReset();
+
mHandlerThread = new HandlerThread(getClass().getSimpleName());
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
- 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,
- () -> {
- CountDownLatch latch = new CountDownLatch(1);
- mThreadNetworkController.setTestNetworkAsUpstream(
- mInfraNetworkTracker.getTestIface().getInterfaceName(),
- MoreExecutors.directExecutor(),
- v -> {
- latch.countDown();
- });
- latch.await();
- });
+ mFtds = new ArrayList<>();
+
+ setUpInfraNetwork();
+ mController.setEnabledAndWait(true);
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ // Creates a infra network device.
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ startInfraDeviceAndWaitForOnLinkAddr();
+
+ // Create Ftds
+ for (int i = 0; i < NUM_FTD; ++i) {
+ mFtds.add(new FullThreadDevice(15 + i /* node ID */));
+ }
}
@After
public void tearDown() throws Exception {
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> {
- 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());
+ mController.setTestNetworkAsUpstreamAndWait(null);
+ mController.leaveAndWait();
+ tearDownInfraNetwork();
mHandlerThread.quitSafely();
mHandlerThread.join();
+
+ for (var ftd : mFtds) {
+ ftd.destroy();
+ }
+ mFtds.clear();
}
@Test
- public void infraDevicePingTheadDeviceOmr_Succeeds() throws Exception {
+ public void unicastRouting_infraDevicePingThreadDeviceOmr_replyReceived() throws Exception {
/*
* <pre>
* Topology:
@@ -140,40 +170,518 @@
* </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 */);
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
- // 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);
+ mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
// Infra device receives an echo reply sent by FTD.
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ }
+
+ @Test
+ public void unicastRouting_afterFactoryResetInfraDevicePingThreadDeviceOmr_replyReceived()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ startInfraDeviceAndWaitForOnLinkAddr();
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ }
+
+ @Test
+ public void unicastRouting_afterInfraNetworkSwitchInfraDevicePingThreadDeviceOmr_replyReceived()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+ // Create a new infra network and let Thread prefer it
+ TestNetworkTracker oldInfraNetworkTracker = mInfraNetworkTracker;
+ try {
+ setUpInfraNetwork();
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ startInfraDeviceAndWaitForOnLinkAddr();
+
+ mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
+ } finally {
+ runAsShell(MANAGE_TEST_NETWORKS, () -> oldInfraNetworkTracker.teardown());
+ }
+ }
+
+ @Test
+ public void unicastRouting_borderRouterSendsUdpToThreadDevice_datagramReceived()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * Thread
+ * Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdOmr = requireNonNull(ftd.getOmrAddress());
+ Inet6Address ftdMlEid = requireNonNull(ftd.getMlEid());
+
+ ftd.udpBind(ftdOmr, 12345);
+ sendUdpMessage(ftdOmr, 12345, "aaaaaaaa");
+ assertEquals("aaaaaaaa", ftd.udpReceive());
+
+ ftd.udpBind(ftdMlEid, 12345);
+ sendUdpMessage(ftdMlEid, 12345, "bbbbbbbb");
+ assertEquals("bbbbbbbb", ftd.udpReceive());
+ }
+
+ @Test
+ public void unicastRouting_meshLocalAddressesAreNotPreferred() throws Exception {
+ // When BR is enabled, there will be OMR address, so the mesh-local addresses are expected
+ // to be deprecated.
+ List<LinkAddress> linkAddresses = getIpv6LinkAddresses("thread-wpan");
+ IpPrefix meshLocalPrefix = DEFAULT_DATASET.getMeshLocalPrefix();
+
+ for (LinkAddress address : linkAddresses) {
+ if (meshLocalPrefix.contains(address.getAddress())) {
+ assertThat(address.getDeprecationTime()).isAtMost(SystemClock.elapsedRealtime());
+ assertThat(address.isPreferred()).isFalse();
+ }
+ }
+ }
+
+ @Test
+ @RequiresIpv6MulticastRouting
+ public void multicastRouting_ftdSubscribedMulticastAddress_infraLinkJoinsMulticastGroup()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_5);
+
+ assertInfraLinkMemberOfGroup(GROUP_ADDR_SCOPE_5);
+ }
+
+ @Test
+ @RequiresIpv6MulticastRouting
+ public void
+ multicastRouting_ftdSubscribedScope3MulticastAddress_infraLinkNotJoinMulticastGroup()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
+
+ assertInfraLinkNotMemberOfGroup(GROUP_ADDR_SCOPE_3);
+ }
+
+ @Test
+ @RequiresIpv6MulticastRouting
+ public void multicastRouting_ftdSubscribedMulticastAddress_canPingfromInfraLink()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ }
+
+ @Test
+ @RequiresIpv6MulticastRouting
+ public void multicastRouting_inboundForwarding_afterBrRejoinFtdRepliesSubscribedAddress()
+ throws Exception {
+
+ // TODO (b/327311034): Testing bbr state switch from primary mode to secondary mode and back
+ // to primary mode requires an additional BR in the Thread network. This is not currently
+ // supported, to be implemented when possible.
+ }
+
+ @Test
+ @RequiresIpv6MulticastRouting
+ public void multicastRouting_ftdSubscribedScope3MulticastAddress_cannotPingfromInfraLink()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ ftd.subscribeMulticastAddress(GROUP_ADDR_SCOPE_3);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_3);
+
+ assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ }
+
+ @Test
+ @RequiresIpv6MulticastRouting
+ public void multicastRouting_ftdNotSubscribedMulticastAddress_cannotPingFromInfraDevice()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
+
+ assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+ }
+
+ @Test
+ @RequiresIpv6MulticastRouting
+ public void multicastRouting_multipleFtdsSubscribedDifferentAddresses_canPingFromInfraDevice()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device 1
+ * (Cuttlefish)
+ * |
+ * | Thread
+ * |
+ * Full Thread device 2
+ * </pre>
+ */
+
+ FullThreadDevice ftd1 = mFtds.get(0);
+ startFtdChild(ftd1);
+ subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
+
+ FullThreadDevice ftd2 = mFtds.get(1);
+ startFtdChild(ftd2);
+ subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_4);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+
+ // Verify ping reply from ftd1 and ftd2 separately as the order of replies can't be
+ // predicted.
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
+ }
+
+ @Test
+ @RequiresIpv6MulticastRouting
+ public void multicastRouting_multipleFtdsSubscribedSameAddress_canPingFromInfraDevice()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device 1
+ * (Cuttlefish)
+ * |
+ * | Thread
+ * |
+ * Full Thread device 2
+ * </pre>
+ */
+
+ FullThreadDevice ftd1 = mFtds.get(0);
+ startFtdChild(ftd1);
+ subscribeMulticastAddressAndWait(ftd1, GROUP_ADDR_SCOPE_5);
+
+ FullThreadDevice ftd2 = mFtds.get(1);
+ startFtdChild(ftd2);
+ subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_5);
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+
+ // Send the request twice as the order of replies from ftd1 and ftd2 are not guaranteed
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
+ }
+
+ @Test
+ @RequiresIpv6MulticastRouting
+ public void multicastRouting_outboundForwarding_scopeLargerThan3IsForwarded() throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+
+ ftd.ping(GROUP_ADDR_SCOPE_5);
+ ftd.ping(GROUP_ADDR_SCOPE_4);
+
assertNotNull(
- readPacketFrom(
- infraNetworkReader,
- p -> isExpectedIcmpv6Packet(p, ICMPV6_ECHO_REPLY_TYPE)));
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
+ assertNotNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+ }
+
+ @Test
+ @RequiresIpv6MulticastRouting
+ public void multicastRouting_outboundForwarding_scopeSmallerThan4IsNotForwarded()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+
+ ftd.ping(GROUP_ADDR_SCOPE_3);
+
+ assertNull(
+ pollForPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftd.getOmrAddress(), GROUP_ADDR_SCOPE_3));
+ }
+
+ @Test
+ @RequiresIpv6MulticastRouting
+ public void multicastRouting_outboundForwarding_llaToScope4IsNotForwarded() throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdLla = ftd.getLinkLocalAddress();
+ assertNotNull(ftdLla);
+
+ ftd.ping(GROUP_ADDR_SCOPE_4, ftdLla);
+
+ assertNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
+ }
+
+ @Test
+ @RequiresIpv6MulticastRouting
+ public void multicastRouting_outboundForwarding_mlaToScope4IsNotForwarded() throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ List<Inet6Address> ftdMlas = ftd.getMeshLocalAddresses();
+ assertFalse(ftdMlas.isEmpty());
+
+ for (Inet6Address ftdMla : ftdMlas) {
+ ftd.ping(GROUP_ADDR_SCOPE_4, ftdMla);
+
+ assertNull(
+ pollForPacketOnInfraNetwork(
+ ICMPV6_ECHO_REQUEST_TYPE, ftdMla, GROUP_ADDR_SCOPE_4));
+ }
+ }
+
+ @Test
+ @RequiresIpv6MulticastRouting
+ public void multicastRouting_infraNetworkSwitch_ftdRepliesToSubscribedAddress()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ subscribeMulticastAddressAndWait(ftd, GROUP_ADDR_SCOPE_5);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+
+ // Destroy infra link and re-create
+ tearDownInfraNetwork();
+ setUpInfraNetwork();
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ startInfraDeviceAndWaitForOnLinkAddr();
+
+ mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
+ assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
+ }
+
+ @Test
+ @RequiresIpv6MulticastRouting
+ public void multicastRouting_infraNetworkSwitch_outboundPacketIsForwarded() throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ FullThreadDevice ftd = mFtds.get(0);
+ startFtdChild(ftd);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+
+ // Destroy infra link and re-create
+ tearDownInfraNetwork();
+ setUpInfraNetwork();
+ mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ startInfraDeviceAndWaitForOnLinkAddr();
+
+ ftd.ping(GROUP_ADDR_SCOPE_4);
+
+ assertNotNull(
+ pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
+ }
+
+ private void setUpInfraNetwork() throws Exception {
+ mInfraNetworkTracker =
+ runAsShell(
+ MANAGE_TEST_NETWORKS,
+ () ->
+ initTestNetwork(
+ mContext, new LinkProperties(), 5000 /* timeoutMs */));
+ mController.setTestNetworkAsUpstreamAndWait(
+ mInfraNetworkTracker.getTestIface().getInterfaceName());
+ }
+
+ private void tearDownInfraNetwork() {
+ runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
+ }
+
+ private void startFtdChild(FullThreadDevice ftd) throws Exception {
+ ftd.factoryReset();
+ ftd.joinNetwork(DEFAULT_DATASET);
+ ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ waitFor(() -> ftd.getOmrAddress() != null, Duration.ofSeconds(60));
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+ assertNotNull(ftdOmr);
+ }
+
+ private void startInfraDeviceAndWaitForOnLinkAddr() throws Exception {
+ mInfraDevice =
+ new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), mInfraNetworkReader);
+ mInfraDevice.runSlaac(Duration.ofSeconds(60));
+ assertNotNull(mInfraDevice.ipv6Addr);
+ }
+
+ private void assertInfraLinkMemberOfGroup(Inet6Address address) throws Exception {
+ waitFor(
+ () ->
+ isInMulticastGroup(
+ mInfraNetworkTracker.getTestIface().getInterfaceName(), address),
+ Duration.ofSeconds(3));
+ }
+
+ private void assertInfraLinkNotMemberOfGroup(Inet6Address address) throws Exception {
+ waitFor(
+ () ->
+ !isInMulticastGroup(
+ mInfraNetworkTracker.getTestIface().getInterfaceName(), address),
+ Duration.ofSeconds(3));
+ }
+
+ private void subscribeMulticastAddressAndWait(FullThreadDevice ftd, Inet6Address address)
+ throws Exception {
+ ftd.subscribeMulticastAddress(address);
+
+ assertInfraLinkMemberOfGroup(address);
+ }
+
+ private byte[] pollForPacketOnInfraNetwork(int type, Inet6Address srcAddress) {
+ return pollForPacketOnInfraNetwork(type, srcAddress, null);
+ }
+
+ private byte[] pollForPacketOnInfraNetwork(
+ int type, Inet6Address srcAddress, Inet6Address destAddress) {
+ Predicate<byte[]> filter;
+ filter =
+ p ->
+ (isExpectedIcmpv6Packet(p, type)
+ && (srcAddress == null ? true : isFromIpv6Source(p, srcAddress))
+ && (destAddress == null
+ ? true
+ : isToIpv6Destination(p, destAddress)));
+ return pollForPacket(mInfraNetworkReader, filter);
}
}
diff --git a/thread/tests/integration/src/android/net/thread/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/FullThreadDevice.java
deleted file mode 100644
index 01638f3..0000000
--- a/thread/tests/integration/src/android/net/thread/FullThreadDevice.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.net.thread;
-
-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/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java
deleted file mode 100644
index 9d9a4ff..0000000
--- a/thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.net.thread;
-
-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 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() {}
-
- /**
- * 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/integration/src/android/net/thread/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
new file mode 100644
index 0000000..5a8d21f
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
+import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.discoverForServiceLost;
+import static android.net.thread.utils.IntegrationTestUtils.discoverService;
+import static android.net.thread.utils.IntegrationTestUtils.resolveService;
+import static android.net.thread.utils.IntegrationTestUtils.resolveServiceUntil;
+import static android.net.thread.utils.IntegrationTestUtils.waitFor;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.content.Context;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.net.thread.utils.FullThreadDevice;
+import android.net.thread.utils.OtDaemonController;
+import android.net.thread.utils.TapTestNetworkTracker;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+import android.net.thread.utils.ThreadNetworkControllerWrapper;
+import android.os.HandlerThread;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.truth.Correspondence;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/** Integration test cases for Service Discovery feature. */
+@RunWith(AndroidJUnit4.class)
+@RequiresThreadFeature
+@RequiresSimulationThreadDevice
+@LargeTest
+public class ServiceDiscoveryTest {
+ private static final String TAG = ServiceDiscoveryTest.class.getSimpleName();
+ private static final int NUM_FTD = 3;
+
+ // 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);
+
+ private static final Correspondence<byte[], byte[]> BYTE_ARRAY_EQUALITY =
+ Correspondence.from(Arrays::equals, "is equivalent to");
+
+ @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final ThreadNetworkControllerWrapper mController =
+ ThreadNetworkControllerWrapper.newInstance(mContext);
+ private final OtDaemonController mOtCtl = new OtDaemonController();
+ private HandlerThread mHandlerThread;
+ private NsdManager mNsdManager;
+ private TapTestNetworkTracker mTestNetworkTracker;
+ private final List<FullThreadDevice> mFtds = new ArrayList<>();
+ private final List<RegistrationListener> mRegistrationListeners = new ArrayList<>();
+
+ @Before
+ public void setUp() throws Exception {
+ mOtCtl.factoryReset();
+ mController.setEnabledAndWait(true);
+ mController.joinAndWait(DEFAULT_DATASET);
+ mNsdManager = mContext.getSystemService(NsdManager.class);
+
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+
+ mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper());
+ assertThat(mTestNetworkTracker).isNotNull();
+ mController.setTestNetworkAsUpstreamAndWait(mTestNetworkTracker.getInterfaceName());
+
+ // Create the FTDs in setUp() so that the FTDs can be safely released in tearDown().
+ // Don't create new FTDs in test cases.
+ for (int i = 0; i < NUM_FTD; ++i) {
+ FullThreadDevice ftd = new FullThreadDevice(10 + i /* node ID */);
+ ftd.autoStartSrpClient();
+ mFtds.add(ftd);
+ }
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ for (RegistrationListener listener : mRegistrationListeners) {
+ unregisterService(listener);
+ }
+ for (FullThreadDevice ftd : mFtds) {
+ // Clear registered SRP hosts and services
+ if (ftd.isSrpHostRegistered()) {
+ ftd.removeSrpHost();
+ }
+ ftd.destroy();
+ }
+ if (mTestNetworkTracker != null) {
+ mTestNetworkTracker.tearDown();
+ }
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
+ }
+ mController.setTestNetworkAsUpstreamAndWait(null);
+ mController.leaveAndWait();
+ }
+
+ @Test
+ public void advertisingProxy_multipleSrpClientsRegisterServices_servicesResolvableByMdns()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * Thread
+ * Border Router -------------- Full Thread device 1
+ * (Cuttlefish) |
+ * +------ Full Thread device 2
+ * |
+ * +------ Full Thread device 3
+ * </pre>
+ */
+
+ // Creates Full Thread Devices (FTD) and let them join the network.
+ for (FullThreadDevice ftd : mFtds) {
+ ftd.joinNetwork(DEFAULT_DATASET);
+ ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ }
+
+ int randomId = new Random().nextInt(10_000);
+
+ String serviceNamePrefix = "service-" + randomId + "-";
+ String serviceTypePrefix = "_test" + randomId;
+ String hostnamePrefix = "host-" + randomId + "-";
+
+ // For every FTD, let it register an SRP service.
+ for (int i = 0; i < mFtds.size(); ++i) {
+ FullThreadDevice ftd = mFtds.get(i);
+ ftd.setSrpHostname(hostnamePrefix + i);
+ ftd.setSrpHostAddresses(List.of(ftd.getOmrAddress(), ftd.getMlEid()));
+ ftd.addSrpService(
+ serviceNamePrefix + i,
+ serviceTypePrefix + i + "._tcp",
+ List.of("_sub1", "_sub2"),
+ 12345 /* port */,
+ Map.of("key1", bytes(0x01, 0x02), "key2", bytes(i)));
+ }
+
+ // Check the advertised services are discoverable and resolvable by NsdManager
+ for (int i = 0; i < mFtds.size(); ++i) {
+ NsdServiceInfo discoveredService =
+ discoverService(mNsdManager, serviceTypePrefix + i + "._tcp");
+ assertThat(discoveredService).isNotNull();
+ NsdServiceInfo resolvedService = resolveService(mNsdManager, discoveredService);
+ assertThat(resolvedService.getServiceName()).isEqualTo(serviceNamePrefix + i);
+ assertThat(resolvedService.getServiceType()).isEqualTo(serviceTypePrefix + i + "._tcp");
+ assertThat(resolvedService.getPort()).isEqualTo(12345);
+ assertThat(resolvedService.getAttributes())
+ .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+ .containsExactly("key1", bytes(0x01, 0x02), "key2", bytes(i));
+ assertThat(resolvedService.getHostname()).isEqualTo(hostnamePrefix + i);
+ assertThat(resolvedService.getHostAddresses())
+ .containsExactly(mFtds.get(i).getOmrAddress());
+ }
+ }
+
+ @Test
+ public void advertisingProxy_srpClientUpdatesService_updatedServiceResolvableByMdns()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * Thread
+ * Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ // Creates a Full Thread Devices (FTD) and let it join the network.
+ FullThreadDevice ftd = mFtds.get(0);
+ ftd.joinNetwork(DEFAULT_DATASET);
+ ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ ftd.setSrpHostname("my-host");
+ ftd.setSrpHostAddresses(List.of((Inet6Address) parseNumericAddress("2001:db8::1")));
+ ftd.addSrpService(
+ "my-service",
+ "_test._tcp",
+ Collections.emptyList() /* subtypes */,
+ 12345 /* port */,
+ Map.of("key1", bytes(0x01, 0x02), "key2", bytes(0x03)));
+
+ // Update the host addresses
+ ftd.setSrpHostAddresses(
+ List.of(
+ (Inet6Address) parseNumericAddress("2001:db8::1"),
+ (Inet6Address) parseNumericAddress("2001:db8::2")));
+ // Update the service
+ ftd.updateSrpService(
+ "my-service", "_test._tcp", List.of("_sub3"), 11111, Map.of("key1", bytes(0x04)));
+ waitFor(ftd::isSrpHostRegistered, SERVICE_DISCOVERY_TIMEOUT);
+
+ // Check the advertised service is discoverable and resolvable by NsdManager
+ NsdServiceInfo discoveredService = discoverService(mNsdManager, "_test._tcp");
+ assertThat(discoveredService).isNotNull();
+ NsdServiceInfo resolvedService =
+ resolveServiceUntil(
+ mNsdManager,
+ discoveredService,
+ s -> s.getPort() == 11111 && s.getHostAddresses().size() == 2);
+ assertThat(resolvedService.getServiceName()).isEqualTo("my-service");
+ assertThat(resolvedService.getServiceType()).isEqualTo("_test._tcp");
+ assertThat(resolvedService.getPort()).isEqualTo(11111);
+ assertThat(resolvedService.getAttributes())
+ .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+ .containsExactly("key1", bytes(0x04));
+ assertThat(resolvedService.getHostname()).isEqualTo("my-host");
+ assertThat(resolvedService.getHostAddresses())
+ .containsExactly(
+ parseNumericAddress("2001:db8::1"), parseNumericAddress("2001:db8::2"));
+ }
+
+ @Test
+ public void advertisingProxy_srpClientUnregistersService_serviceIsNotDiscoverableByMdns()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * Thread
+ * Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ // Creates a Full Thread Devices (FTD) and let it join the network.
+ FullThreadDevice ftd = mFtds.get(0);
+ ftd.joinNetwork(DEFAULT_DATASET);
+ ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ ftd.setSrpHostname("my-host");
+ ftd.setSrpHostAddresses(
+ List.of(
+ (Inet6Address) parseNumericAddress("2001:db8::1"),
+ (Inet6Address) parseNumericAddress("2001:db8::2")));
+ ftd.addSrpService(
+ "my-service",
+ "_test._udp",
+ List.of("_sub1"),
+ 12345 /* port */,
+ Map.of("key1", bytes(0x01, 0x02), "key2", bytes(0x03)));
+ // Wait for the service to be discoverable by NsdManager.
+ assertThat(discoverService(mNsdManager, "_test._udp")).isNotNull();
+
+ // Unregister the service.
+ CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
+ NsdManager.DiscoveryListener listener =
+ discoverForServiceLost(mNsdManager, "_test._udp", serviceLostFuture);
+ ftd.removeSrpService("my-service", "_test._udp", true /* notifyServer */);
+
+ // Verify the service becomes lost.
+ try {
+ serviceLostFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+ } finally {
+ mNsdManager.stopServiceDiscovery(listener);
+ }
+ assertThrows(TimeoutException.class, () -> discoverService(mNsdManager, "_test._udp"));
+ }
+
+ @Test
+ public void meshcopOverlay_vendorAndModelNameAreSetToOverlayValue() throws Exception {
+ NsdServiceInfo discoveredService = discoverService(mNsdManager, "_meshcop._udp");
+ assertThat(discoveredService).isNotNull();
+ NsdServiceInfo meshcopService = resolveService(mNsdManager, discoveredService);
+
+ Map<String, byte[]> txtMap = meshcopService.getAttributes();
+ assertThat(txtMap.get("vn")).isEqualTo("Android".getBytes(UTF_8));
+ assertThat(txtMap.get("mn")).isEqualTo("Thread Border Router".getBytes(UTF_8));
+ }
+
+ @Test
+ @Ignore("TODO: b/332452386 - Enable this test case when it handles the multi-client case well")
+ public void discoveryProxy_multipleClientsBrowseAndResolveServiceOverMdns() throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * Thread
+ * Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ RegistrationListener listener = new RegistrationListener();
+ NsdServiceInfo info = new NsdServiceInfo();
+ info.setServiceType("_testservice._tcp");
+ info.setServiceName("test-service");
+ info.setPort(12345);
+ info.setHostname("testhost");
+ info.setHostAddresses(List.of(parseNumericAddress("2001::1")));
+ info.setAttribute("key1", bytes(0x01, 0x02));
+ info.setAttribute("key2", bytes(0x03));
+ registerService(info, listener);
+ mRegistrationListeners.add(listener);
+ for (int i = 0; i < NUM_FTD; ++i) {
+ FullThreadDevice ftd = mFtds.get(i);
+ ftd.joinNetwork(DEFAULT_DATASET);
+ ftd.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ ftd.setDnsServerAddress(mOtCtl.getMlEid().getHostAddress());
+ }
+ final ArrayList<NsdServiceInfo> browsedServices = new ArrayList<>();
+ final ArrayList<NsdServiceInfo> resolvedServices = new ArrayList<>();
+ final ArrayList<Thread> threads = new ArrayList<>();
+ for (int i = 0; i < NUM_FTD; ++i) {
+ browsedServices.add(null);
+ resolvedServices.add(null);
+ }
+ for (int i = 0; i < NUM_FTD; ++i) {
+ final FullThreadDevice ftd = mFtds.get(i);
+ final int index = i;
+ Runnable task =
+ () -> {
+ browsedServices.set(
+ index,
+ ftd.browseService("_testservice._tcp.default.service.arpa."));
+ resolvedServices.set(
+ index,
+ ftd.resolveService(
+ "test-service", "_testservice._tcp.default.service.arpa."));
+ };
+ threads.add(new Thread(task));
+ }
+ for (Thread thread : threads) {
+ thread.start();
+ }
+ for (Thread thread : threads) {
+ thread.join();
+ }
+
+ for (int i = 0; i < NUM_FTD; ++i) {
+ NsdServiceInfo browsedService = browsedServices.get(i);
+ assertThat(browsedService.getServiceName()).isEqualTo("test-service");
+ assertThat(browsedService.getPort()).isEqualTo(12345);
+
+ NsdServiceInfo resolvedService = resolvedServices.get(i);
+ assertThat(resolvedService.getServiceName()).isEqualTo("test-service");
+ assertThat(resolvedService.getPort()).isEqualTo(12345);
+ assertThat(resolvedService.getHostname()).isEqualTo("testhost.default.service.arpa.");
+ assertThat(resolvedService.getHostAddresses())
+ .containsExactly(parseNumericAddress("2001::1"));
+ assertThat(resolvedService.getAttributes())
+ .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+ .containsExactly("key1", bytes(0x01, 0x02), "key2", bytes(3));
+ }
+ }
+
+ @Test
+ public void discoveryProxy_browseAndResolveServiceAtSrpServer() throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * Thread
+ * Border Router -------+------ SRP client
+ * (Cuttlefish) |
+ * +------ DNS client
+ *
+ * </pre>
+ */
+ FullThreadDevice srpClient = mFtds.get(0);
+ srpClient.joinNetwork(DEFAULT_DATASET);
+ srpClient.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ srpClient.setSrpHostname("my-host");
+ srpClient.setSrpHostAddresses(List.of((Inet6Address) parseNumericAddress("2001::1")));
+ srpClient.addSrpService(
+ "my-service",
+ "_test._udp",
+ List.of("_sub1"),
+ 12345 /* port */,
+ Map.of("key1", bytes(0x01, 0x02), "key2", bytes(0x03)));
+
+ FullThreadDevice dnsClient = mFtds.get(1);
+ dnsClient.joinNetwork(DEFAULT_DATASET);
+ dnsClient.waitForStateAnyOf(List.of("router", "child"), JOIN_TIMEOUT);
+ dnsClient.setDnsServerAddress(mOtCtl.getMlEid().getHostAddress());
+
+ NsdServiceInfo browsedService = dnsClient.browseService("_test._udp.default.service.arpa.");
+ assertThat(browsedService.getServiceName()).isEqualTo("my-service");
+ assertThat(browsedService.getPort()).isEqualTo(12345);
+ assertThat(browsedService.getHostname()).isEqualTo("my-host.default.service.arpa.");
+ assertThat(browsedService.getHostAddresses())
+ .containsExactly(parseNumericAddress("2001::1"));
+ assertThat(browsedService.getAttributes())
+ .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+ .containsExactly("key1", bytes(0x01, 0x02), "key2", bytes(3));
+
+ NsdServiceInfo resolvedService =
+ dnsClient.resolveService("my-service", "_test._udp.default.service.arpa.");
+ assertThat(resolvedService.getServiceName()).isEqualTo("my-service");
+ assertThat(resolvedService.getPort()).isEqualTo(12345);
+ assertThat(resolvedService.getHostname()).isEqualTo("my-host.default.service.arpa.");
+ assertThat(resolvedService.getHostAddresses())
+ .containsExactly(parseNumericAddress("2001::1"));
+ assertThat(resolvedService.getAttributes())
+ .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+ .containsExactly("key1", bytes(0x01, 0x02), "key2", bytes(3));
+ }
+
+ private void registerService(NsdServiceInfo serviceInfo, RegistrationListener listener)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ mNsdManager.registerService(serviceInfo, PROTOCOL_DNS_SD, listener);
+ listener.waitForRegistered();
+ }
+
+ private void unregisterService(RegistrationListener listener)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ mNsdManager.unregisterService(listener);
+ listener.waitForUnregistered();
+ }
+
+ private static class RegistrationListener implements NsdManager.RegistrationListener {
+ private final CompletableFuture<Void> mRegisteredFuture = new CompletableFuture<>();
+ private final CompletableFuture<Void> mUnRegisteredFuture = new CompletableFuture<>();
+
+ RegistrationListener() {}
+
+ @Override
+ public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {}
+
+ @Override
+ public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {}
+
+ @Override
+ public void onServiceRegistered(NsdServiceInfo serviceInfo) {
+ mRegisteredFuture.complete(null);
+ }
+
+ @Override
+ public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
+ mUnRegisteredFuture.complete(null);
+ }
+
+ public void waitForRegistered()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ mRegisteredFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+ }
+
+ public void waitForUnregistered()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ mUnRegisteredFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+ }
+ }
+
+ private static byte[] bytes(int... byteInts) {
+ byte[] bytes = new byte[byteInts.length];
+ for (int i = 0; i < byteInts.length; ++i) {
+ bytes[i] = (byte) byteInts[i];
+ }
+ return bytes;
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
new file mode 100644
index 0000000..1410d41
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
+import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
+import static android.net.thread.utils.IntegrationTestUtils.waitFor;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.thread.utils.FullThreadDevice;
+import android.net.thread.utils.OtDaemonController;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+import android.net.thread.utils.ThreadNetworkControllerWrapper;
+import android.os.SystemClock;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/** Tests for E2E Android Thread integration with ot-daemon, ConnectivityService, etc.. */
+@LargeTest
+@RequiresThreadFeature
+@RunWith(AndroidJUnit4.class)
+public class ThreadIntegrationTest {
+ // The byte[] buffer size for UDP tests
+ private static final int UDP_BUFFER_SIZE = 1024;
+
+ // The maximum time for OT addresses to be propagated to the TUN interface "thread-wpan"
+ private static final Duration TUN_ADDR_UPDATE_TIMEOUT = Duration.ofSeconds(1);
+
+ // 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);
+
+ @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+ private ExecutorService mExecutor;
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final ThreadNetworkControllerWrapper mController =
+ ThreadNetworkControllerWrapper.newInstance(mContext);
+ private OtDaemonController mOtCtl;
+ private FullThreadDevice mFtd;
+
+ @Before
+ public void setUp() throws Exception {
+ mExecutor = Executors.newSingleThreadExecutor();
+ mOtCtl = new OtDaemonController();
+ mController.leaveAndWait();
+
+ // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
+ mOtCtl.factoryReset();
+
+ mFtd = new FullThreadDevice(10 /* nodeId */);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mController.setTestNetworkAsUpstreamAndWait(null);
+ mController.leaveAndWait();
+
+ mFtd.destroy();
+ mExecutor.shutdownNow();
+ }
+
+ @Test
+ public void otDaemonRestart_notJoinedAndStopped_deviceRoleIsStopped() throws Exception {
+ mController.leaveAndWait();
+
+ runShellCommand("stop ot-daemon");
+ // TODO(b/323331973): the sleep is needed to workaround the race conditions
+ SystemClock.sleep(200);
+
+ mController.waitForRole(DEVICE_ROLE_STOPPED, CALLBACK_TIMEOUT);
+ }
+
+ @Test
+ public void otDaemonRestart_JoinedNetworkAndStopped_autoRejoinedAndTunIfStateConsistent()
+ throws Exception {
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ runShellCommand("stop ot-daemon");
+
+ mController.waitForRole(DEVICE_ROLE_DETACHED, CALLBACK_TIMEOUT);
+ mController.waitForRole(DEVICE_ROLE_LEADER, RESTART_JOIN_TIMEOUT);
+ assertThat(mOtCtl.isInterfaceUp()).isTrue();
+ assertThat(runShellCommand("ifconfig thread-wpan")).contains("UP POINTOPOINT RUNNING");
+ }
+
+ @Test
+ public void otDaemonFactoryReset_deviceRoleIsStopped() throws Exception {
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ mOtCtl.factoryReset();
+
+ assertThat(mController.getDeviceRole()).isEqualTo(DEVICE_ROLE_STOPPED);
+ }
+
+ @Test
+ public void otDaemonFactoryReset_addressesRemoved() throws Exception {
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ mOtCtl.factoryReset();
+
+ String ifconfig = runShellCommand("ifconfig thread-wpan");
+ assertThat(ifconfig).doesNotContain("inet6 addr");
+ }
+
+ // TODO (b/323300829): add test for removing an OT address
+ @Test
+ public void tunInterface_joinedNetwork_otAddressesAddedToTunInterface() throws Exception {
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ List<Inet6Address> otAddresses = mOtCtl.getAddresses();
+ assertThat(otAddresses).isNotEmpty();
+ // TODO: it's cleaner to have a retry() method to retry failed asserts in given delay so
+ // that we can write assertThat() in the Predicate
+ waitFor(
+ () -> {
+ String ifconfig = runShellCommand("ifconfig thread-wpan");
+ return otAddresses.stream()
+ .allMatch(addr -> ifconfig.contains(addr.getHostAddress()));
+ },
+ TUN_ADDR_UPDATE_TIMEOUT);
+ }
+
+ @Test
+ public void otDaemonRestart_latestCountryCodeIsSetToOtDaemon() throws Exception {
+ runThreadCommand("force-country-code enabled CN");
+
+ runShellCommand("stop ot-daemon");
+ // TODO(b/323331973): the sleep is needed to workaround the race conditions
+ SystemClock.sleep(200);
+ mController.waitForRole(DEVICE_ROLE_STOPPED, CALLBACK_TIMEOUT);
+
+ assertThat(mOtCtl.getCountryCode()).isEqualTo("CN");
+ }
+
+ @Test
+ public void udp_appStartEchoServer_endDeviceUdpEchoSuccess() throws Exception {
+ // Topology:
+ // Test App ------ thread-wpan ------ End Device
+
+ mController.joinAndWait(DEFAULT_DATASET);
+ startFtdChild(mFtd, DEFAULT_DATASET);
+ final Inet6Address serverAddress = mOtCtl.getMeshLocalAddresses().get(0);
+ final int serverPort = 9527;
+
+ mExecutor.execute(() -> startUdpEchoServerAndWait(serverAddress, serverPort));
+ mFtd.udpOpen();
+ mFtd.udpSend("Hello,Thread", serverAddress, serverPort);
+ String udpReply = mFtd.udpReceive();
+
+ assertThat(udpReply).isEqualTo("Hello,Thread");
+ }
+
+ @Test
+ public void joinNetworkWithBrDisabled_meshLocalAddressesArePreferred() throws Exception {
+ // When BR feature is disabled, there is no OMR address, so the mesh-local addresses are
+ // expected to be preferred.
+ mOtCtl.executeCommand("br disable");
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ IpPrefix meshLocalPrefix = DEFAULT_DATASET.getMeshLocalPrefix();
+ List<LinkAddress> linkAddresses = getIpv6LinkAddresses("thread-wpan");
+ for (LinkAddress address : linkAddresses) {
+ if (meshLocalPrefix.contains(address.getAddress())) {
+ assertThat(address.getDeprecationTime())
+ .isGreaterThan(SystemClock.elapsedRealtime());
+ assertThat(address.isPreferred()).isTrue();
+ }
+ }
+
+ mOtCtl.executeCommand("br enable");
+ }
+
+ // TODO (b/323300829): add more tests for integration with linux platform and
+ // ConnectivityService
+
+ private static String runThreadCommand(String cmd) {
+ return runShellCommandOrThrow("cmd thread_network " + cmd);
+ }
+
+ private void startFtdChild(FullThreadDevice ftd, ActiveOperationalDataset activeDataset)
+ throws Exception {
+ ftd.factoryReset();
+ ftd.joinNetwork(activeDataset);
+ ftd.waitForStateAnyOf(List.of("router", "child"), Duration.ofSeconds(8));
+ }
+
+ /**
+ * Starts a UDP echo server and replies to the first UDP message.
+ *
+ * <p>This method exits when the first UDP message is received and the reply is sent
+ */
+ private void startUdpEchoServerAndWait(InetAddress serverAddress, int serverPort) {
+ try (var udpServerSocket = new DatagramSocket(serverPort, serverAddress)) {
+ DatagramPacket recvPacket =
+ new DatagramPacket(new byte[UDP_BUFFER_SIZE], UDP_BUFFER_SIZE);
+ udpServerSocket.receive(recvPacket);
+ byte[] sendBuffer = Arrays.copyOf(recvPacket.getData(), recvPacket.getData().length);
+ udpServerSocket.send(
+ new DatagramPacket(
+ sendBuffer,
+ sendBuffer.length,
+ (Inet6Address) recvPacket.getAddress(),
+ recvPacket.getPort()));
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java
new file mode 100644
index 0000000..ba04348
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+import android.os.OutcomeReceiver;
+import android.util.SparseIntArray;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/** Tests for hide methods of {@link ThreadNetworkController}. */
+@LargeTest
+@RequiresThreadFeature
+@RunWith(AndroidJUnit4.class)
+public class ThreadNetworkControllerTest {
+ private static final int VALID_POWER = 32_767;
+ private static final int INVALID_POWER = 32_768;
+ private static final int VALID_CHANNEL = 20;
+ private static final int INVALID_CHANNEL = 10;
+ private static final String THREAD_NETWORK_PRIVILEGED =
+ "android.permission.THREAD_NETWORK_PRIVILEGED";
+
+ private static final SparseIntArray CHANNEL_MAX_POWERS =
+ new SparseIntArray() {
+ {
+ put(20, 32767);
+ }
+ };
+
+ @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private ExecutorService mExecutor;
+ private ThreadNetworkController mController;
+
+ @Before
+ public void setUp() throws Exception {
+ mController =
+ mContext.getSystemService(ThreadNetworkManager.class)
+ .getAllThreadNetworkControllers()
+ .get(0);
+
+ mExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ dropAllPermissions();
+ }
+
+ @Test
+ public void setChannelMaxPowers_withPrivilegedPermission_success() throws Exception {
+ CompletableFuture<Void> powerFuture = new CompletableFuture<>();
+
+ runAsShell(
+ THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.setChannelMaxPowers(
+ CHANNEL_MAX_POWERS, mExecutor, newOutcomeReceiver(powerFuture)));
+
+ try {
+ assertThat(powerFuture.get()).isNull();
+ } catch (ExecutionException exception) {
+ ThreadNetworkException thrown = (ThreadNetworkException) exception.getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_UNSUPPORTED_OPERATION);
+ }
+ }
+
+ @Test
+ public void setChannelMaxPowers_withoutPrivilegedPermission_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ assertThrows(
+ SecurityException.class,
+ () -> mController.setChannelMaxPowers(CHANNEL_MAX_POWERS, mExecutor, v -> {}));
+ }
+
+ @Test
+ public void setChannelMaxPowers_emptyChannelMaxPower_throwsIllegalArgumentException() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.setChannelMaxPowers(new SparseIntArray(), mExecutor, v -> {}));
+ }
+
+ @Test
+ public void setChannelMaxPowers_invalidChannel_throwsIllegalArgumentException() {
+ final SparseIntArray INVALID_CHANNEL_ARRAY =
+ new SparseIntArray() {
+ {
+ put(INVALID_CHANNEL, VALID_POWER);
+ }
+ };
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.setChannelMaxPowers(INVALID_CHANNEL_ARRAY, mExecutor, v -> {}));
+ }
+
+ @Test
+ public void setChannelMaxPowers_invalidPower_throwsIllegalArgumentException() {
+ final SparseIntArray INVALID_POWER_ARRAY =
+ new SparseIntArray() {
+ {
+ put(VALID_CHANNEL, INVALID_POWER);
+ }
+ };
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> mController.setChannelMaxPowers(INVALID_POWER_ARRAY, mExecutor, v -> {}));
+ }
+
+ private static void dropAllPermissions() {
+ getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+ }
+
+ private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
+ CompletableFuture<V> future) {
+ return new OutcomeReceiver<V, ThreadNetworkException>() {
+ @Override
+ public void onResult(V result) {
+ future.complete(result);
+ }
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ future.completeExceptionally(e);
+ }
+ };
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
new file mode 100644
index 0000000..8835f40
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
+import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
+import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+import android.net.thread.utils.ThreadNetworkControllerWrapper;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.ExecutionException;
+
+/** Integration tests for {@link ThreadNetworkShellCommand}. */
+@LargeTest
+@RequiresThreadFeature
+@RunWith(AndroidJUnit4.class)
+public class ThreadNetworkShellCommandTest {
+ @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final ThreadNetworkControllerWrapper mController =
+ ThreadNetworkControllerWrapper.newInstance(mContext);
+
+ @Before
+ public void setUp() {
+ ensureThreadEnabled();
+ }
+
+ @After
+ public void tearDown() {
+ ensureThreadEnabled();
+ }
+
+ private static void ensureThreadEnabled() {
+ runThreadCommand("force-stop-ot-daemon disabled");
+ runThreadCommand("enable");
+ }
+
+ @Test
+ public void enable_threadStateIsEnabled() throws Exception {
+ runThreadCommand("enable");
+
+ assertThat(mController.getEnabledState()).isEqualTo(STATE_ENABLED);
+ }
+
+ @Test
+ public void disable_threadStateIsDisabled() throws Exception {
+ runThreadCommand("disable");
+
+ assertThat(mController.getEnabledState()).isEqualTo(STATE_DISABLED);
+ }
+
+ @Test
+ public void forceStopOtDaemon_forceStopEnabled_otDaemonServiceDisappear() {
+ runThreadCommand("force-stop-ot-daemon enabled");
+
+ assertThat(runShellCommandOrThrow("service list")).doesNotContain("ot_daemon");
+ }
+
+ @Test
+ public void forceStopOtDaemon_forceStopEnabled_canNotEnableThread() throws Exception {
+ runThreadCommand("force-stop-ot-daemon enabled");
+
+ ExecutionException thrown =
+ assertThrows(ExecutionException.class, () -> mController.setEnabledAndWait(true));
+ ThreadNetworkException cause = (ThreadNetworkException) thrown.getCause();
+ assertThat(cause.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED);
+ }
+
+ @Test
+ public void forceStopOtDaemon_forceStopDisabled_otDaemonServiceAppears() throws Exception {
+ runThreadCommand("force-stop-ot-daemon disabled");
+
+ assertThat(runShellCommandOrThrow("service list")).contains("ot_daemon");
+ }
+
+ @Test
+ public void forceStopOtDaemon_forceStopDisabled_canEnableThread() throws Exception {
+ runThreadCommand("force-stop-ot-daemon disabled");
+
+ mController.setEnabledAndWait(true);
+ assertThat(mController.getEnabledState()).isEqualTo(STATE_ENABLED);
+ }
+
+ @Test
+ public void forceCountryCode_setCN_getCountryCodeReturnsCN() {
+ runThreadCommand("force-country-code enabled CN");
+
+ final String result = runThreadCommand("get-country-code");
+ assertThat(result).contains("Thread country code = CN");
+ }
+
+ private static String runThreadCommand(String cmd) {
+ return runShellCommandOrThrow("cmd thread_network " + cmd);
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
new file mode 100644
index 0000000..5e70f6c
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -0,0 +1,552 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread.utils;
+
+import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.waitFor;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.nsd.NsdServiceInfo;
+import android.net.thread.ActiveOperationalDataset;
+
+import com.google.errorprone.annotations.FormatMethod;
+
+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.net.InetAddress;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 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 static final int HOP_LIMIT = 64;
+ private static final int PING_INTERVAL = 1;
+ private static final int PING_SIZE = 100;
+ // There may not be a response for the ping command, using a short timeout to keep the tests
+ // short.
+ private static final float PING_TIMEOUT_SECONDS = 0.1f;
+
+ private 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;
+ }
+
+ public void destroy() {
+ mProcess.destroy();
+ }
+
+ /**
+ * 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;
+ }
+
+ /** Returns the Mesh-local EID address on this device if any. */
+ public Inet6Address getMlEid() {
+ List<String> addresses = executeCommand("ipaddr mleid");
+ return (Inet6Address) InetAddresses.parseNumericAddress(addresses.get(0));
+ }
+
+ /**
+ * Returns the link-local address of the device.
+ *
+ * <p>This methods goes through all unicast addresses on the device and returns the address that
+ * begins with fe80.
+ */
+ public Inet6Address getLinkLocalAddress() {
+ List<String> output = executeCommand("ipaddr linklocal");
+ if (!output.isEmpty() && output.get(0).startsWith("fe80:")) {
+ return (Inet6Address) InetAddresses.parseNumericAddress(output.get(0));
+ }
+ return null;
+ }
+
+ /**
+ * Returns the mesh-local addresses of the device.
+ *
+ * <p>This methods goes through all unicast addresses on the device and returns the address that
+ * begins with mesh-local prefix.
+ */
+ public List<Inet6Address> getMeshLocalAddresses() {
+ List<String> addresses = executeCommand("ipaddr");
+ List<Inet6Address> meshLocalAddresses = new ArrayList<>();
+ IpPrefix meshLocalPrefix = mActiveOperationalDataset.getMeshLocalPrefix();
+ for (String address : addresses) {
+ Inet6Address addr = (Inet6Address) InetAddresses.parseNumericAddress(address);
+ if (meshLocalPrefix.contains(addr)) {
+ meshLocalAddresses.add(addr);
+ }
+ }
+ return meshLocalAddresses;
+ }
+
+ /**
+ * 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 timeout the time to wait for the expected state before throwing
+ */
+ public void waitForStateAnyOf(List<String> states, Duration timeout) throws TimeoutException {
+ waitFor(() -> states.contains(getState()), timeout);
+ }
+
+ /**
+ * Gets the state of the Thread device.
+ *
+ * @return a string representing the state.
+ */
+ public String getState() {
+ return executeCommand("state").get(0);
+ }
+
+ /** Closes the UDP socket. */
+ public void udpClose() {
+ executeCommand("udp close");
+ }
+
+ /** Opens the UDP socket. */
+ public void udpOpen() {
+ executeCommand("udp open");
+ }
+
+ /** Opens the UDP socket and binds it to a specific address and port. */
+ public void udpBind(Inet6Address address, int port) {
+ udpClose();
+ udpOpen();
+ executeCommand("udp bind %s %d", address.getHostAddress(), port);
+ }
+
+ /** Returns the message received on the UDP socket. */
+ public String udpReceive() throws IOException {
+ Pattern pattern =
+ Pattern.compile("> (\\d+) bytes from ([\\da-f:]+) (\\d+) ([\\x00-\\x7F]+)");
+ Matcher matcher = pattern.matcher(mReader.readLine());
+ matcher.matches();
+
+ return matcher.group(4);
+ }
+
+ /** Sends a UDP message to given IPv6 address and port. */
+ public void udpSend(String message, Inet6Address serverAddr, int serverPort) {
+ executeCommand("udp send %s %d %s", serverAddr.getHostAddress(), serverPort, message);
+ }
+
+ /** Enables the SRP client and run in autostart mode. */
+ public void autoStartSrpClient() {
+ executeCommand("srp client autostart enable");
+ }
+
+ /** Sets the hostname (e.g. "MyHost") for the SRP client. */
+ public void setSrpHostname(String hostname) {
+ executeCommand("srp client host name " + hostname);
+ }
+
+ /** Sets the host addresses for the SRP client. */
+ public void setSrpHostAddresses(List<Inet6Address> addresses) {
+ executeCommand(
+ "srp client host address "
+ + String.join(
+ " ",
+ addresses.stream().map(Inet6Address::getHostAddress).toList()));
+ }
+
+ /** Removes the SRP host */
+ public void removeSrpHost() {
+ executeCommand("srp client host remove 1 1");
+ }
+
+ /**
+ * Adds an SRP service for the SRP client and wait for the registration to complete.
+ *
+ * @param serviceName the service name like "MyService"
+ * @param serviceType the service type like "_test._tcp"
+ * @param subtypes the service subtypes like "_sub1"
+ * @param port the port number in range [1, 65535]
+ * @param txtMap the map of TXT names and values
+ * @throws TimeoutException if the service isn't registered within timeout
+ */
+ public void addSrpService(
+ String serviceName,
+ String serviceType,
+ List<String> subtypes,
+ int port,
+ Map<String, byte[]> txtMap)
+ throws TimeoutException {
+ StringBuilder fullServiceType = new StringBuilder(serviceType);
+ for (String subtype : subtypes) {
+ fullServiceType.append(",").append(subtype);
+ }
+ executeCommand(
+ "srp client service add %s %s %d %d %d %s",
+ serviceName,
+ fullServiceType,
+ port,
+ 0 /* priority */,
+ 0 /* weight */,
+ txtMapToHexString(txtMap));
+ waitFor(() -> isSrpServiceRegistered(serviceName, serviceType), SERVICE_DISCOVERY_TIMEOUT);
+ }
+
+ /**
+ * Removes an SRP service for the SRP client.
+ *
+ * @param serviceName the service name like "MyService"
+ * @param serviceType the service type like "_test._tcp"
+ * @param notifyServer whether to notify SRP server about the removal
+ */
+ public void removeSrpService(String serviceName, String serviceType, boolean notifyServer) {
+ String verb = notifyServer ? "remove" : "clear";
+ executeCommand("srp client service %s %s %s", verb, serviceName, serviceType);
+ }
+
+ /**
+ * Updates an existing SRP service for the SRP client.
+ *
+ * <p>This is essentially a 'remove' and an 'add' on the SRP client's side.
+ *
+ * @param serviceName the service name like "MyService"
+ * @param serviceType the service type like "_test._tcp"
+ * @param subtypes the service subtypes like "_sub1"
+ * @param port the port number in range [1, 65535]
+ * @param txtMap the map of TXT names and values
+ * @throws TimeoutException if the service isn't updated within timeout
+ */
+ public void updateSrpService(
+ String serviceName,
+ String serviceType,
+ List<String> subtypes,
+ int port,
+ Map<String, byte[]> txtMap)
+ throws TimeoutException {
+ removeSrpService(serviceName, serviceType, false /* notifyServer */);
+ addSrpService(serviceName, serviceType, subtypes, port, txtMap);
+ }
+
+ /** Checks if an SRP service is registered. */
+ public boolean isSrpServiceRegistered(String serviceName, String serviceType) {
+ List<String> lines = executeCommand("srp client service");
+ for (String line : lines) {
+ if (line.contains(serviceName) && line.contains(serviceType)) {
+ return line.contains("Registered");
+ }
+ }
+ return false;
+ }
+
+ /** Checks if an SRP host is registered. */
+ public boolean isSrpHostRegistered() {
+ List<String> lines = executeCommand("srp client host");
+ for (String line : lines) {
+ return line.contains("Registered");
+ }
+ return false;
+ }
+
+ /** Sets the DNS server address. */
+ public void setDnsServerAddress(String address) {
+ executeCommand("dns config " + address);
+ }
+
+ /** Returns the first browsed service instance of {@code serviceType}. */
+ public NsdServiceInfo browseService(String serviceType) {
+ // CLI output:
+ // DNS browse response for _testservice._tcp.default.service.arpa.
+ // test-service
+ // Port:12345, Priority:0, Weight:0, TTL:10
+ // Host:testhost.default.service.arpa.
+ // HostAddress:2001:0:0:0:0:0:0:1 TTL:10
+ // TXT:[key1=0102, key2=03] TTL:10
+
+ List<String> lines = executeCommand("dns browse " + serviceType);
+ NsdServiceInfo info = new NsdServiceInfo();
+ info.setServiceName(lines.get(1));
+ info.setServiceType(serviceType);
+ info.setPort(DnsServiceCliOutputParser.parsePort(lines.get(2)));
+ info.setHostname(DnsServiceCliOutputParser.parseHostname(lines.get(3)));
+ info.setHostAddresses(List.of(DnsServiceCliOutputParser.parseHostAddress(lines.get(4))));
+ DnsServiceCliOutputParser.parseTxtIntoServiceInfo(lines.get(5), info);
+
+ return info;
+ }
+
+ /** Returns the resolved service instance. */
+ public NsdServiceInfo resolveService(String serviceName, String serviceType) {
+ // CLI output:
+ // DNS service resolution response for test-service for service
+ // _test._tcp.default.service.arpa.
+ // Port:12345, Priority:0, Weight:0, TTL:10
+ // Host:Android.default.service.arpa.
+ // HostAddress:2001:0:0:0:0:0:0:1 TTL:10
+ // TXT:[key1=0102, key2=03] TTL:10
+
+ List<String> lines = executeCommand("dns service %s %s", serviceName, serviceType);
+ NsdServiceInfo info = new NsdServiceInfo();
+ info.setServiceName(serviceName);
+ info.setServiceType(serviceType);
+ info.setPort(DnsServiceCliOutputParser.parsePort(lines.get(1)));
+ info.setHostname(DnsServiceCliOutputParser.parseHostname(lines.get(2)));
+ info.setHostAddresses(List.of(DnsServiceCliOutputParser.parseHostAddress(lines.get(3))));
+ DnsServiceCliOutputParser.parseTxtIntoServiceInfo(lines.get(4), info);
+
+ return info;
+ }
+
+ /** 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);
+ }
+ }
+
+ public void subscribeMulticastAddress(Inet6Address address) {
+ executeCommand("ipmaddr add " + address.getHostAddress());
+ }
+
+ public void ping(Inet6Address address, Inet6Address source) {
+ ping(
+ address,
+ source,
+ PING_SIZE,
+ 1 /* count */,
+ PING_INTERVAL,
+ HOP_LIMIT,
+ PING_TIMEOUT_SECONDS);
+ }
+
+ public void ping(Inet6Address address) {
+ ping(
+ address,
+ null,
+ PING_SIZE,
+ 1 /* count */,
+ PING_INTERVAL,
+ HOP_LIMIT,
+ PING_TIMEOUT_SECONDS);
+ }
+
+ private void ping(
+ Inet6Address address,
+ Inet6Address source,
+ int size,
+ int count,
+ int interval,
+ int hopLimit,
+ float timeout) {
+ String cmd =
+ "ping"
+ + ((source == null) ? "" : (" -I " + source.getHostAddress()))
+ + " "
+ + address.getHostAddress()
+ + " "
+ + size
+ + " "
+ + count
+ + " "
+ + interval
+ + " "
+ + hopLimit
+ + " "
+ + timeout;
+ executeCommand(cmd);
+ }
+
+ @FormatMethod
+ private List<String> executeCommand(String commandFormat, Object... args) {
+ return executeCommand(String.format(commandFormat, args));
+ }
+
+ 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")) {
+ throw new IOException("ot-cli-ftd reported an error: " + line);
+ }
+ if (!line.startsWith("> ")) {
+ result.add(line);
+ }
+ }
+ return result;
+ }
+
+ private static String txtMapToHexString(Map<String, byte[]> txtMap) {
+ if (txtMap == null) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry<String, byte[]> entry : txtMap.entrySet()) {
+ int length = entry.getKey().length() + entry.getValue().length + 1;
+ sb.append(String.format("%02x", length));
+ sb.append(toHexString(entry.getKey()));
+ sb.append(toHexString("="));
+ sb.append(toHexString(entry.getValue()));
+ }
+ return sb.toString();
+ }
+
+ private static String toHexString(String s) {
+ return toHexString(s.getBytes(StandardCharsets.UTF_8));
+ }
+
+ private static String toHexString(byte[] bytes) {
+ return base16().encode(bytes);
+ }
+
+ private static final class DnsServiceCliOutputParser {
+ /** Returns the first match in the input of a given regex pattern. */
+ private static Matcher firstMatchOf(String input, String regex) {
+ Matcher matcher = Pattern.compile(regex).matcher(input);
+ matcher.find();
+ return matcher;
+ }
+
+ // Example: "Port:12345"
+ private static int parsePort(String line) {
+ return Integer.parseInt(firstMatchOf(line, "Port:(\\d+)").group(1));
+ }
+
+ // Example: "Host:Android.default.service.arpa."
+ private static String parseHostname(String line) {
+ return firstMatchOf(line, "Host:(.+)").group(1);
+ }
+
+ // Example: "HostAddress:2001:0:0:0:0:0:0:1"
+ private static InetAddress parseHostAddress(String line) {
+ return InetAddresses.parseNumericAddress(
+ firstMatchOf(line, "HostAddress:([^ ]+)").group(1));
+ }
+
+ // Example: "TXT:[key1=0102, key2=03]"
+ private static void parseTxtIntoServiceInfo(String line, NsdServiceInfo serviceInfo) {
+ String txtString = firstMatchOf(line, "TXT:\\[([^\\]]+)\\]").group(1);
+ for (String txtEntry : txtString.split(",")) {
+ String[] nameAndValue = txtEntry.trim().split("=");
+ String name = nameAndValue[0];
+ String value = nameAndValue[1];
+ byte[] bytes = new byte[value.length() / 2];
+ for (int i = 0; i < value.length(); i += 2) {
+ byte b = (byte) ((value.charAt(i) - '0') << 4 | (value.charAt(i + 1) - '0'));
+ bytes[i / 2] = b;
+ }
+ serviceInfo.setAttribute(name, bytes);
+ }
+ }
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java b/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
similarity index 90%
rename from thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java
rename to thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
index 43a800d..72a278c 100644
--- a/thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.net.thread;
+package android.net.thread.utils;
-import static android.net.thread.IntegrationTestUtils.getRaPios;
-import static android.net.thread.IntegrationTestUtils.readPacketFrom;
-import static android.net.thread.IntegrationTestUtils.waitFor;
+import static android.net.thread.utils.IntegrationTestUtils.getRaPios;
+import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
+import static android.net.thread.utils.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;
@@ -34,6 +34,7 @@
import java.net.Inet6Address;
import java.net.InetAddress;
import java.nio.ByteBuffer;
+import java.time.Duration;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeoutException;
@@ -100,15 +101,15 @@
* @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 */);
+ public void runSlaac(Duration timeout) throws TimeoutException {
+ waitFor(() -> (ipv6Addr = runSlaac()) != null, timeout);
}
private Inet6Address runSlaac() {
try {
sendRsPacket();
- final byte[] raPacket = readPacketFrom(packetReader, p -> !getRaPios(p).isEmpty());
+ final byte[] raPacket = pollForPacket(packetReader, p -> !getRaPios(p).isEmpty());
final List<PrefixInformationOption> options = getRaPios(raPacket);
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
new file mode 100644
index 0000000..9be9566
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread.utils;
+
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.net.InetAddresses;
+import android.net.LinkAddress;
+import android.net.TestNetworkInterface;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.net.thread.ThreadNetworkController;
+import android.os.Handler;
+import android.os.SystemClock;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
+import com.android.testutils.HandlerUtils;
+import com.android.testutils.TapPacketReader;
+
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/** Static utility methods relating to Thread integration tests. */
+public final class IntegrationTestUtils {
+ // The timeout of join() after restarting ot-daemon. The device needs to send 6 Link Request
+ // every 5 seconds, followed by 4 Parent Request every second. So this value needs to be 40
+ // seconds to be safe
+ public static final Duration RESTART_JOIN_TIMEOUT = Duration.ofSeconds(40);
+ public static final Duration JOIN_TIMEOUT = Duration.ofSeconds(30);
+ public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
+ public static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
+ public static final Duration SERVICE_DISCOVERY_TIMEOUT = Duration.ofSeconds(20);
+
+ private IntegrationTestUtils() {}
+
+ /**
+ * Waits for the given {@link Supplier} to be true until given timeout.
+ *
+ * @param condition the condition to check
+ * @param timeout the time to wait for the condition before throwing
+ * @throws TimeoutException if the condition is still not met when the timeout expires
+ */
+ public static void waitFor(Supplier<Boolean> condition, Duration timeout)
+ throws TimeoutException {
+ final long intervalMills = 500;
+ final long timeoutMills = timeout.toMillis();
+
+ for (long i = 0; i < timeoutMills; i += intervalMills) {
+ if (condition.get()) {
+ return;
+ }
+ SystemClock.sleep(intervalMills);
+ }
+ if (condition.get()) {
+ return;
+ }
+ throw new TimeoutException("The condition failed to become true in " + timeout);
+ }
+
+ /**
+ * Creates a {@link TapPacketReader} given the {@link TestNetworkInterface} and {@link Handler}.
+ *
+ * @param testNetworkInterface the TUN interface of the test network
+ * @param handler the handler to process the packets
+ * @return the {@link TapPacketReader}
+ */
+ public static TapPacketReader newPacketReader(
+ TestNetworkInterface testNetworkInterface, Handler handler) {
+ FileDescriptor fd = testNetworkInterface.getFileDescriptor().getFileDescriptor();
+ final TapPacketReader reader =
+ new TapPacketReader(handler, fd, testNetworkInterface.getMtu());
+ handler.post(() -> reader.start());
+ HandlerUtils.waitForIdle(handler, 5000 /* timeout in milliseconds */);
+ return reader;
+ }
+
+ /**
+ * Waits for the Thread module to enter any state of the given {@code deviceRoles}.
+ *
+ * @param controller the {@link ThreadNetworkController}
+ * @param deviceRoles the desired device roles. See also {@link
+ * ThreadNetworkController.DeviceRole}
+ * @param timeout the time to wait for the expected state before throwing
+ * @return the {@link ThreadNetworkController.DeviceRole} after waiting
+ * @throws TimeoutException if the device hasn't become any of expected roles until the timeout
+ * expires
+ */
+ public static int waitForStateAnyOf(
+ ThreadNetworkController controller, List<Integer> deviceRoles, Duration timeout)
+ throws TimeoutException {
+ SettableFuture<Integer> future = SettableFuture.create();
+ ThreadNetworkController.StateCallback callback =
+ newRole -> {
+ if (deviceRoles.contains(newRole)) {
+ future.set(newRole);
+ }
+ };
+ controller.registerStateCallback(directExecutor(), callback);
+ try {
+ return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
+ } catch (InterruptedException | ExecutionException e) {
+ throw new TimeoutException(
+ String.format(
+ "The device didn't become an expected role in %s: %s",
+ timeout, e.getMessage()));
+ } finally {
+ controller.unregisterStateCallback(callback);
+ }
+ }
+
+ /**
+ * Polls for a packet from a given {@link TapPacketReader} that satisfies the {@code filter}.
+ *
+ * @param packetReader a TUN packet reader
+ * @param filter the filter to be applied on the packet
+ * @return the first IPv6 packet that satisfies the {@code filter}. If it has waited for more
+ * than 3000ms to read the next packet, the method will return null
+ */
+ public static byte[] pollForPacket(TapPacketReader packetReader, Predicate<byte[]> filter) {
+ byte[] packet;
+ while ((packet = packetReader.poll(3000 /* timeoutMs */, filter)) != null) {
+ return packet;
+ }
+ return null;
+ }
+
+ /** Returns {@code true} if {@code packet} is an ICMPv6 packet of given {@code type}. */
+ public static boolean isExpectedIcmpv6Packet(byte[] packet, int type) {
+ if (packet == null) {
+ return false;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ try {
+ if (Struct.parse(Ipv6Header.class, buf).nextHeader != (byte) IPPROTO_ICMPV6) {
+ return false;
+ }
+ return Struct.parse(Icmpv6Header.class, buf).type == (short) type;
+ } catch (IllegalArgumentException ignored) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false;
+ }
+
+ public static boolean isFromIpv6Source(byte[] packet, Inet6Address src) {
+ if (packet == null) {
+ return false;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ try {
+ return Struct.parse(Ipv6Header.class, buf).srcIp.equals(src);
+ } catch (IllegalArgumentException ignored) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false;
+ }
+
+ public static boolean isToIpv6Destination(byte[] packet, Inet6Address dest) {
+ if (packet == null) {
+ return false;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ try {
+ return Struct.parse(Ipv6Header.class, buf).dstIp.equals(dest);
+ } catch (IllegalArgumentException ignored) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false;
+ }
+
+ /** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
+ public static List<PrefixInformationOption> getRaPios(byte[] raMsg) {
+ final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
+
+ if (raMsg == null) {
+ return pioList;
+ }
+
+ final ByteBuffer buf = ByteBuffer.wrap(raMsg);
+ final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class, buf);
+ if (ipv6Header.nextHeader != (byte) IPPROTO_ICMPV6) {
+ return pioList;
+ }
+
+ final Icmpv6Header icmpv6Header = Struct.parse(Icmpv6Header.class, buf);
+ if (icmpv6Header.type != (short) ICMPV6_ROUTER_ADVERTISEMENT) {
+ return pioList;
+ }
+
+ Struct.parse(RaHeader.class, buf);
+ while (buf.position() < raMsg.length) {
+ final int currentPos = buf.position();
+ final int type = Byte.toUnsignedInt(buf.get());
+ final int length = Byte.toUnsignedInt(buf.get());
+ if (type == ICMPV6_ND_OPTION_PIO) {
+ final ByteBuffer pioBuf =
+ ByteBuffer.wrap(
+ buf.array(),
+ currentPos,
+ Struct.getSize(PrefixInformationOption.class));
+ final PrefixInformationOption pio =
+ Struct.parse(PrefixInformationOption.class, pioBuf);
+ pioList.add(pio);
+
+ // Move ByteBuffer position to the next option.
+ buf.position(currentPos + Struct.getSize(PrefixInformationOption.class));
+ } else {
+ // The length is in units of 8 octets.
+ buf.position(currentPos + (length * 8));
+ }
+ }
+ return pioList;
+ }
+
+ /**
+ * Sends a UDP message to a destination.
+ *
+ * @param dstAddress the IP address of the destination
+ * @param dstPort the port of the destination
+ * @param message the message in UDP payload
+ * @throws IOException if failed to send the message
+ */
+ public static void sendUdpMessage(InetAddress dstAddress, int dstPort, String message)
+ throws IOException {
+ SocketAddress dstSockAddr = new InetSocketAddress(dstAddress, dstPort);
+
+ try (DatagramSocket socket = new DatagramSocket()) {
+ socket.connect(dstSockAddr);
+
+ byte[] msgBytes = message.getBytes();
+ DatagramPacket packet = new DatagramPacket(msgBytes, msgBytes.length);
+
+ socket.send(packet);
+ }
+ }
+
+ public static boolean isInMulticastGroup(String interfaceName, Inet6Address address) {
+ final String cmd = "ip -6 maddr show dev " + interfaceName;
+ final String output = runShellCommandOrThrow(cmd);
+ final String addressStr = address.getHostAddress();
+ for (final String line : output.split("\\n")) {
+ if (line.contains(addressStr)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static List<LinkAddress> getIpv6LinkAddresses(String interfaceName) throws IOException {
+ List<LinkAddress> addresses = new ArrayList<>();
+ final String cmd = " ip -6 addr show dev " + interfaceName;
+ final String output = runShellCommandOrThrow(cmd);
+
+ for (final String line : output.split("\\n")) {
+ if (line.contains("inet6")) {
+ addresses.add(parseAddressLine(line));
+ }
+ }
+
+ return addresses;
+ }
+
+ /** Return the first discovered service of {@code serviceType}. */
+ public static NsdServiceInfo discoverService(NsdManager nsdManager, String serviceType)
+ throws Exception {
+ CompletableFuture<NsdServiceInfo> serviceInfoFuture = new CompletableFuture<>();
+ NsdManager.DiscoveryListener listener =
+ new DefaultDiscoveryListener() {
+ @Override
+ public void onServiceFound(NsdServiceInfo serviceInfo) {
+ serviceInfoFuture.complete(serviceInfo);
+ }
+ };
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
+ try {
+ serviceInfoFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+ } finally {
+ nsdManager.stopServiceDiscovery(listener);
+ }
+
+ return serviceInfoFuture.get();
+ }
+
+ /**
+ * Returns the {@link NsdServiceInfo} when a service instance of {@code serviceType} gets lost.
+ */
+ public static NsdManager.DiscoveryListener discoverForServiceLost(
+ NsdManager nsdManager,
+ String serviceType,
+ CompletableFuture<NsdServiceInfo> serviceInfoFuture) {
+ NsdManager.DiscoveryListener listener =
+ new DefaultDiscoveryListener() {
+ @Override
+ public void onServiceLost(NsdServiceInfo serviceInfo) {
+ serviceInfoFuture.complete(serviceInfo);
+ }
+ };
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
+ return listener;
+ }
+
+ /** Resolves the service. */
+ public static NsdServiceInfo resolveService(NsdManager nsdManager, NsdServiceInfo serviceInfo)
+ throws Exception {
+ return resolveServiceUntil(nsdManager, serviceInfo, s -> true);
+ }
+
+ /** Returns the first resolved service that satisfies the {@code predicate}. */
+ public static NsdServiceInfo resolveServiceUntil(
+ NsdManager nsdManager, NsdServiceInfo serviceInfo, Predicate<NsdServiceInfo> predicate)
+ throws Exception {
+ CompletableFuture<NsdServiceInfo> resolvedServiceInfoFuture = new CompletableFuture<>();
+ NsdManager.ServiceInfoCallback callback =
+ new DefaultServiceInfoCallback() {
+ @Override
+ public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
+ if (predicate.test(serviceInfo)) {
+ resolvedServiceInfoFuture.complete(serviceInfo);
+ }
+ }
+ };
+ nsdManager.registerServiceInfoCallback(serviceInfo, directExecutor(), callback);
+ try {
+ return resolvedServiceInfoFuture.get(
+ SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+ } finally {
+ nsdManager.unregisterServiceInfoCallback(callback);
+ }
+ }
+
+ private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener {
+ @Override
+ public void onStartDiscoveryFailed(String serviceType, int errorCode) {}
+
+ @Override
+ public void onStopDiscoveryFailed(String serviceType, int errorCode) {}
+
+ @Override
+ public void onDiscoveryStarted(String serviceType) {}
+
+ @Override
+ public void onDiscoveryStopped(String serviceType) {}
+
+ @Override
+ public void onServiceFound(NsdServiceInfo serviceInfo) {}
+
+ @Override
+ public void onServiceLost(NsdServiceInfo serviceInfo) {}
+ }
+
+ private static class DefaultServiceInfoCallback implements NsdManager.ServiceInfoCallback {
+ @Override
+ public void onServiceInfoCallbackRegistrationFailed(int errorCode) {}
+
+ @Override
+ public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {}
+
+ @Override
+ public void onServiceLost() {}
+
+ @Override
+ public void onServiceInfoCallbackUnregistered() {}
+ }
+
+ /**
+ * Parses a line of output from "ip -6 addr show" into a {@link LinkAddress}.
+ *
+ * <p>Example line: "inet6 2001:db8:1:1::1/64 scope global deprecated"
+ */
+ private static LinkAddress parseAddressLine(String line) {
+ String[] parts = line.trim().split("\\s+");
+ String addressString = parts[1];
+ String[] pieces = addressString.split("/", 2);
+ int prefixLength = Integer.parseInt(pieces[1]);
+ final InetAddress address = InetAddresses.parseNumericAddress(pieces[0]);
+ long deprecationTimeMillis =
+ line.contains("deprecated")
+ ? SystemClock.elapsedRealtime()
+ : LinkAddress.LIFETIME_PERMANENT;
+
+ return new LinkAddress(
+ address,
+ prefixLength,
+ 0 /* flags */,
+ 0 /* scope */,
+ deprecationTimeMillis,
+ LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
new file mode 100644
index 0000000..b3175fd
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread.utils;
+
+import android.annotation.Nullable;
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.os.SystemClock;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import java.net.Inet6Address;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Wrapper of the "/system/bin/ot-ctl" which can be used to send CLI commands to ot-daemon to
+ * control its behavior.
+ *
+ * <p>Note that this class takes root privileged to run.
+ */
+public final class OtDaemonController {
+ private static final String OT_CTL = "/system/bin/ot-ctl";
+
+ /**
+ * Factory resets ot-daemon.
+ *
+ * <p>This will erase all persistent data written into apexdata/com.android.apex/ot-daemon and
+ * restart the ot-daemon service.
+ */
+ public void factoryReset() {
+ executeCommand("factoryreset");
+
+ // TODO(b/323164524): ot-ctl is a separate process so that the tests can't depend on the
+ // time sequence. Here needs to wait for system server to receive the ot-daemon death
+ // signal and take actions.
+ // A proper fix is to replace "ot-ctl" with "cmd thread_network ot-ctl" which is
+ // synchronized with the system server
+ SystemClock.sleep(500);
+ }
+
+ /** Returns the list of IPv6 addresses on ot-daemon. */
+ public List<Inet6Address> getAddresses() {
+ return executeCommandAndParse("ipaddr").stream()
+ .map(addr -> InetAddresses.parseNumericAddress(addr))
+ .map(inetAddr -> (Inet6Address) inetAddr)
+ .toList();
+ }
+
+ /** Returns {@code true} if the Thread interface is up. */
+ public boolean isInterfaceUp() {
+ String output = executeCommand("ifconfig");
+ return output.contains("up");
+ }
+
+ /** Returns the ML-EID of the device. */
+ public Inet6Address getMlEid() {
+ String addressStr = executeCommandAndParse("ipaddr mleid").get(0);
+ return (Inet6Address) InetAddresses.parseNumericAddress(addressStr);
+ }
+
+ /** Returns the country code on ot-daemon. */
+ public String getCountryCode() {
+ return executeCommandAndParse("region").get(0);
+ }
+
+ /**
+ * Returns the list of IPv6 Mesh-Local addresses on ot-daemon.
+ *
+ * <p>The return List can be empty if no Mesh-Local prefix exists.
+ */
+ public List<Inet6Address> getMeshLocalAddresses() {
+ IpPrefix meshLocalPrefix = getMeshLocalPrefix();
+ if (meshLocalPrefix == null) {
+ return Collections.emptyList();
+ }
+ return getAddresses().stream().filter(addr -> meshLocalPrefix.contains(addr)).toList();
+ }
+
+ /**
+ * Returns the Mesh-Local prefix or {@code null} if none exists (e.g. the Active Dataset is not
+ * set).
+ */
+ @Nullable
+ public IpPrefix getMeshLocalPrefix() {
+ List<IpPrefix> prefixes =
+ executeCommandAndParse("prefix meshlocal").stream()
+ .map(prefix -> new IpPrefix(prefix))
+ .toList();
+ return prefixes.isEmpty() ? null : prefixes.get(0);
+ }
+
+ public String executeCommand(String cmd) {
+ return SystemUtil.runShellCommand(OT_CTL + " " + cmd);
+ }
+
+ /**
+ * Executes a ot-ctl command and parse the output to a list of strings.
+ *
+ * <p>The trailing "Done" in the command output will be dropped.
+ */
+ public List<String> executeCommandAndParse(String cmd) {
+ return Arrays.asList(executeCommand(cmd).split("\n")).stream()
+ .map(String::trim)
+ .filter(str -> !str.equals("Done"))
+ .toList();
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
new file mode 100644
index 0000000..7e84233
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread.utils;
+
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkController.StateCallback;
+import android.net.thread.ThreadNetworkException;
+import android.net.thread.ThreadNetworkManager;
+import android.os.OutcomeReceiver;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/** A helper class which provides synchronous API wrappers for {@link ThreadNetworkController}. */
+public final class ThreadNetworkControllerWrapper {
+ public static final Duration JOIN_TIMEOUT = Duration.ofSeconds(10);
+ public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
+ private static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
+ private static final Duration SET_ENABLED_TIMEOUT = Duration.ofSeconds(2);
+
+ private final ThreadNetworkController mController;
+
+ /**
+ * Returns a new {@link ThreadNetworkControllerWrapper} instance or {@code null} if Thread
+ * feature is not supported on this device.
+ */
+ @Nullable
+ public static ThreadNetworkControllerWrapper newInstance(Context context) {
+ final ThreadNetworkManager manager = context.getSystemService(ThreadNetworkManager.class);
+ if (manager == null) {
+ return null;
+ }
+ return new ThreadNetworkControllerWrapper(manager.getAllThreadNetworkControllers().get(0));
+ }
+
+ private ThreadNetworkControllerWrapper(ThreadNetworkController controller) {
+ mController = controller;
+ }
+
+ /**
+ * Returns the Thread enabled state.
+ *
+ * <p>The value can be one of {@code ThreadNetworkController#STATE_*}.
+ */
+ public final int getEnabledState()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ CompletableFuture<Integer> future = new CompletableFuture<>();
+ StateCallback callback =
+ new StateCallback() {
+ @Override
+ public void onThreadEnableStateChanged(int enabledState) {
+ future.complete(enabledState);
+ }
+
+ @Override
+ public void onDeviceRoleChanged(int deviceRole) {}
+ };
+
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> mController.registerStateCallback(directExecutor(), callback));
+ try {
+ return future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS);
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+ }
+ }
+
+ /**
+ * Returns the Thread device role.
+ *
+ * <p>The value can be one of {@code ThreadNetworkController#DEVICE_ROLE_*}.
+ */
+ public final int getDeviceRole()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ CompletableFuture<Integer> future = new CompletableFuture<>();
+ StateCallback callback = future::complete;
+
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> mController.registerStateCallback(directExecutor(), callback));
+ try {
+ return future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS);
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+ }
+ }
+
+ /** An synchronous variant of {@link ThreadNetworkController#setEnabled}. */
+ public void setEnabledAndWait(boolean enabled)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.setEnabled(
+ enabled, directExecutor(), newOutcomeReceiver(future)));
+ future.get(SET_ENABLED_TIMEOUT.toSeconds(), SECONDS);
+ }
+
+ /** Joins the given network and wait for this device to become attached. */
+ public void joinAndWait(ActiveOperationalDataset activeDataset)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.join(
+ activeDataset, directExecutor(), newOutcomeReceiver(future)));
+ future.get(JOIN_TIMEOUT.toSeconds(), SECONDS);
+ }
+
+ /** An synchronous variant of {@link ThreadNetworkController#leave}. */
+ public void leaveAndWait() throws InterruptedException, ExecutionException, TimeoutException {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> mController.leave(directExecutor(), future::complete));
+ future.get(LEAVE_TIMEOUT.toSeconds(), SECONDS);
+ }
+
+ /** Waits for the device role to become {@code deviceRole}. */
+ public int waitForRole(int deviceRole, Duration timeout)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return waitForRoleAnyOf(List.of(deviceRole), timeout);
+ }
+
+ /** Waits for the device role to become one of the values specified in {@code deviceRoles}. */
+ public int waitForRoleAnyOf(List<Integer> deviceRoles, Duration timeout)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ CompletableFuture<Integer> future = new CompletableFuture<>();
+ ThreadNetworkController.StateCallback callback =
+ newRole -> {
+ if (deviceRoles.contains(newRole)) {
+ future.complete(newRole);
+ }
+ };
+
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> mController.registerStateCallback(directExecutor(), callback));
+
+ try {
+ return future.get(timeout.toSeconds(), SECONDS);
+ } finally {
+ mController.unregisterStateCallback(callback);
+ }
+ }
+
+ /** An synchronous variant of {@link ThreadNetworkController#setTestNetworkAsUpstream}. */
+ public void setTestNetworkAsUpstreamAndWait(@Nullable String networkInterfaceName)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ NETWORK_SETTINGS,
+ () -> {
+ mController.setTestNetworkAsUpstream(
+ networkInterfaceName, directExecutor(), future::complete);
+ });
+ future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS);
+ }
+
+ private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
+ CompletableFuture<V> future) {
+ return new OutcomeReceiver<V, ThreadNetworkException>() {
+ @Override
+ public void onResult(V result) {
+ future.complete(result);
+ }
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ future.completeExceptionally(e);
+ }
+ };
+ }
+}
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
index 291475e..3365cd0 100644
--- a/thread/tests/unit/Android.bp
+++ b/thread/tests/unit/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_thread_network",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -29,6 +30,7 @@
],
test_suites: [
"general-tests",
+ "mts-tethering",
],
static_libs: [
"frameworks-base-testutils",
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
index 26813c1..d16e423 100644
--- a/thread/tests/unit/AndroidTest.xml
+++ b/thread/tests/unit/AndroidTest.xml
@@ -19,6 +19,18 @@
<option name="test-tag" value="ThreadNetworkUnitTests" />
<option name="test-suite-tag" value="apct" />
+ <!--
+ Only run tests if the device under test is SDK version 34 (Android 14) or above.
+ -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.Sdk34ModuleController" />
+
+ <!-- Run tests in MTS only if the Tethering Mainline module is installed. -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="test-file-name" value="ThreadNetworkUnitTests.apk" />
<option name="check-min-sdk" value="true" />
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
index 75eb043..ac74372 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -19,6 +19,7 @@
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
import static android.os.Process.SYSTEM_UID;
import static com.google.common.io.BaseEncoding.base16;
@@ -33,6 +34,7 @@
import android.os.Binder;
import android.os.OutcomeReceiver;
import android.os.Process;
+import android.util.SparseIntArray;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -77,6 +79,13 @@
private static final ActiveOperationalDataset DEFAULT_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+ private static final SparseIntArray DEFAULT_CHANNEL_POWERS =
+ new SparseIntArray() {
+ {
+ put(20, 32767);
+ }
+ };
+
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -111,6 +120,10 @@
return (IOperationReceiver) invocation.getArguments()[1];
}
+ private static IOperationReceiver getSetChannelMaxPowersReceiver(InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[1];
+ }
+
private static IActiveOperationalDatasetReceiver getCreateDatasetReceiver(
InvocationOnMock invocation) {
return (IActiveOperationalDatasetReceiver) invocation.getArguments()[1];
@@ -361,6 +374,51 @@
}
@Test
+ public void setChannelMaxPowers_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+ setBinderUid(SYSTEM_UID);
+
+ AtomicInteger successCallbackUid = new AtomicInteger(0);
+ AtomicInteger errorCallbackUid = new AtomicInteger(0);
+
+ doAnswer(
+ invoke -> {
+ getSetChannelMaxPowersReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .setChannelMaxPowers(any(ChannelMaxPower[].class), any(IOperationReceiver.class));
+ mController.setChannelMaxPowers(
+ DEFAULT_CHANNEL_POWERS,
+ Runnable::run,
+ v -> successCallbackUid.set(Binder.getCallingUid()));
+ doAnswer(
+ invoke -> {
+ getSetChannelMaxPowersReceiver(invoke)
+ .onError(ERROR_UNSUPPORTED_OPERATION, "");
+ return null;
+ })
+ .when(mMockService)
+ .setChannelMaxPowers(any(ChannelMaxPower[].class), any(IOperationReceiver.class));
+ mController.setChannelMaxPowers(
+ DEFAULT_CHANNEL_POWERS,
+ Runnable::run,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Void unused) {}
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ errorCallbackUid.set(Binder.getCallingUid());
+ }
+ });
+
+ assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+ assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+ }
+
+ @Test
public void setTestNetworkAsUpstream_callbackIsInvokedWithCallingAppIdentity()
throws Exception {
setBinderUid(SYSTEM_UID);
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java
new file mode 100644
index 0000000..5908c20
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link ThreadNetworkException} to cover what is not covered in CTS tests. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ThreadNetworkExceptionTest {
+ @Test
+ public void constructor_tooLargeErrorCode_throwsIllegalArgumentException() throws Exception {
+ // TODO (b/323791003): move this test case to cts/ThreadNetworkExceptionTest when mainline
+ // CTS is ready.
+ assertThrows(IllegalArgumentException.class, () -> new ThreadNetworkException(14, "14"));
+ }
+}
diff --git a/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java
deleted file mode 100644
index 11aabb8..0000000
--- a/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.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/NsdPublisherTest.java b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
new file mode 100644
index 0000000..8886c73
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
@@ -0,0 +1,785 @@
+/*
+ * 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 android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
+import static android.net.nsd.NsdManager.PROTOCOL_DNS_SD;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.InetAddresses;
+import android.net.nsd.DiscoveryRequest;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Handler;
+import android.os.test.TestLooper;
+
+import com.android.server.thread.openthread.DnsTxtAttribute;
+import com.android.server.thread.openthread.INsdDiscoverServiceCallback;
+import com.android.server.thread.openthread.INsdResolveServiceCallback;
+import com.android.server.thread.openthread.INsdStatusReceiver;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/** Unit tests for {@link NsdPublisher}. */
+public final class NsdPublisherTest {
+ @Mock private NsdManager mMockNsdManager;
+
+ @Mock private INsdStatusReceiver mRegistrationReceiver;
+ @Mock private INsdStatusReceiver mUnregistrationReceiver;
+ @Mock private INsdDiscoverServiceCallback mDiscoverServiceCallback;
+ @Mock private INsdResolveServiceCallback mResolveServiceCallback;
+
+ private TestLooper mTestLooper;
+ private NsdPublisher mNsdPublisher;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void registerService_nsdManagerSucceeds_serviceRegistrationSucceeds() throws Exception {
+ prepareTest();
+
+ DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+ DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+ mNsdPublisher.registerService(
+ null,
+ "MyService",
+ "_test._tcp",
+ List.of("_subtype1", "_subtype2"),
+ 12345,
+ List.of(txt1, txt2),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+ ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+ verify(mMockNsdManager, times(1))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(),
+ actualRegistrationListenerCaptor.capture());
+
+ NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+ NsdManager.RegistrationListener actualRegistrationListener =
+ actualRegistrationListenerCaptor.getValue();
+
+ actualRegistrationListener.onServiceRegistered(actualServiceInfo);
+ mTestLooper.dispatchAll();
+
+ assertThat(actualServiceInfo.getServiceName()).isEqualTo("MyService");
+ assertThat(actualServiceInfo.getServiceType()).isEqualTo("_test._tcp");
+ assertThat(actualServiceInfo.getSubtypes()).isEqualTo(Set.of("_subtype1", "_subtype2"));
+ assertThat(actualServiceInfo.getPort()).isEqualTo(12345);
+ assertThat(actualServiceInfo.getAttributes().size()).isEqualTo(2);
+ assertThat(actualServiceInfo.getAttributes().get("key1"))
+ .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x02});
+ assertThat(actualServiceInfo.getAttributes().get("key2"))
+ .isEqualTo(new byte[] {(byte) 0x03});
+
+ verify(mRegistrationReceiver, times(1)).onSuccess();
+ }
+
+ @Test
+ public void registerService_nsdManagerFails_serviceRegistrationFails() throws Exception {
+ prepareTest();
+
+ DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+ DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+ mNsdPublisher.registerService(
+ null,
+ "MyService",
+ "_test._tcp",
+ List.of("_subtype1", "_subtype2"),
+ 12345,
+ List.of(txt1, txt2),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+ ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+ verify(mMockNsdManager, times(1))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(Executor.class),
+ actualRegistrationListenerCaptor.capture());
+
+ NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+ NsdManager.RegistrationListener actualRegistrationListener =
+ actualRegistrationListenerCaptor.getValue();
+
+ actualRegistrationListener.onRegistrationFailed(actualServiceInfo, FAILURE_INTERNAL_ERROR);
+ mTestLooper.dispatchAll();
+
+ assertThat(actualServiceInfo.getServiceName()).isEqualTo("MyService");
+ assertThat(actualServiceInfo.getServiceType()).isEqualTo("_test._tcp");
+ assertThat(actualServiceInfo.getSubtypes()).isEqualTo(Set.of("_subtype1", "_subtype2"));
+ assertThat(actualServiceInfo.getPort()).isEqualTo(12345);
+ assertThat(actualServiceInfo.getAttributes().size()).isEqualTo(2);
+ assertThat(actualServiceInfo.getAttributes().get("key1"))
+ .isEqualTo(new byte[] {(byte) 0x01, (byte) 0x02});
+ assertThat(actualServiceInfo.getAttributes().get("key2"))
+ .isEqualTo(new byte[] {(byte) 0x03});
+
+ verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
+ }
+
+ @Test
+ public void registerService_nsdManagerThrows_serviceRegistrationFails() throws Exception {
+ prepareTest();
+
+ DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+ DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+ doThrow(new IllegalArgumentException("NsdManager fails"))
+ .when(mMockNsdManager)
+ .registerService(any(), anyInt(), any(Executor.class), any());
+
+ mNsdPublisher.registerService(
+ null,
+ "MyService",
+ "_test._tcp",
+ List.of("_subtype1", "_subtype2"),
+ 12345,
+ List.of(txt1, txt2),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+ mTestLooper.dispatchAll();
+
+ verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
+ }
+
+ @Test
+ public void unregisterService_nsdManagerSucceeds_serviceUnregistrationSucceeds()
+ throws Exception {
+ prepareTest();
+
+ DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+ DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+ mNsdPublisher.registerService(
+ null,
+ "MyService",
+ "_test._tcp",
+ List.of("_subtype1", "_subtype2"),
+ 12345,
+ List.of(txt1, txt2),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+ ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+ verify(mMockNsdManager, times(1))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(Executor.class),
+ actualRegistrationListenerCaptor.capture());
+
+ NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+ NsdManager.RegistrationListener actualRegistrationListener =
+ actualRegistrationListenerCaptor.getValue();
+
+ actualRegistrationListener.onServiceRegistered(actualServiceInfo);
+ mNsdPublisher.unregister(mUnregistrationReceiver, 16 /* listenerId */);
+ mTestLooper.dispatchAll();
+ verify(mMockNsdManager, times(1)).unregisterService(actualRegistrationListener);
+
+ actualRegistrationListener.onServiceUnregistered(actualServiceInfo);
+ mTestLooper.dispatchAll();
+ verify(mUnregistrationReceiver, times(1)).onSuccess();
+ }
+
+ @Test
+ public void unregisterService_nsdManagerFails_serviceUnregistrationFails() throws Exception {
+ prepareTest();
+
+ DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+ DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+ mNsdPublisher.registerService(
+ null,
+ "MyService",
+ "_test._tcp",
+ List.of("_subtype1", "_subtype2"),
+ 12345,
+ List.of(txt1, txt2),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+ ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+ verify(mMockNsdManager, times(1))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(Executor.class),
+ actualRegistrationListenerCaptor.capture());
+
+ NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+ NsdManager.RegistrationListener actualRegistrationListener =
+ actualRegistrationListenerCaptor.getValue();
+
+ actualRegistrationListener.onServiceRegistered(actualServiceInfo);
+ mNsdPublisher.unregister(mUnregistrationReceiver, 16 /* listenerId */);
+ mTestLooper.dispatchAll();
+ verify(mMockNsdManager, times(1)).unregisterService(actualRegistrationListener);
+
+ actualRegistrationListener.onUnregistrationFailed(
+ actualServiceInfo, FAILURE_INTERNAL_ERROR);
+ mTestLooper.dispatchAll();
+ verify(mUnregistrationReceiver, times(1)).onError(0);
+ }
+
+ @Test
+ public void registerHost_nsdManagerSucceeds_serviceRegistrationSucceeds() throws Exception {
+ prepareTest();
+
+ mNsdPublisher.registerHost(
+ "MyHost",
+ List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+ ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+ verify(mMockNsdManager, times(1))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(),
+ actualRegistrationListenerCaptor.capture());
+
+ NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+ NsdManager.RegistrationListener actualRegistrationListener =
+ actualRegistrationListenerCaptor.getValue();
+
+ actualRegistrationListener.onServiceRegistered(actualServiceInfo);
+ mTestLooper.dispatchAll();
+
+ assertThat(actualServiceInfo.getServiceName()).isNull();
+ assertThat(actualServiceInfo.getServiceType()).isNull();
+ assertThat(actualServiceInfo.getSubtypes()).isEmpty();
+ assertThat(actualServiceInfo.getPort()).isEqualTo(0);
+ assertThat(actualServiceInfo.getAttributes()).isEmpty();
+ assertThat(actualServiceInfo.getHostname()).isEqualTo("MyHost");
+ assertThat(actualServiceInfo.getHostAddresses())
+ .isEqualTo(makeAddresses("2001:db8::1", "2001:db8::2", "2001:db8::3"));
+
+ verify(mRegistrationReceiver, times(1)).onSuccess();
+ }
+
+ @Test
+ public void registerHost_nsdManagerFails_serviceRegistrationFails() throws Exception {
+ prepareTest();
+
+ mNsdPublisher.registerHost(
+ "MyHost",
+ List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+ ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+ verify(mMockNsdManager, times(1))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(),
+ actualRegistrationListenerCaptor.capture());
+ mTestLooper.dispatchAll();
+
+ NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+ NsdManager.RegistrationListener actualRegistrationListener =
+ actualRegistrationListenerCaptor.getValue();
+
+ actualRegistrationListener.onRegistrationFailed(actualServiceInfo, FAILURE_INTERNAL_ERROR);
+ mTestLooper.dispatchAll();
+
+ assertThat(actualServiceInfo.getServiceName()).isNull();
+ assertThat(actualServiceInfo.getServiceType()).isNull();
+ assertThat(actualServiceInfo.getSubtypes()).isEmpty();
+ assertThat(actualServiceInfo.getPort()).isEqualTo(0);
+ assertThat(actualServiceInfo.getAttributes()).isEmpty();
+ assertThat(actualServiceInfo.getHostname()).isEqualTo("MyHost");
+ assertThat(actualServiceInfo.getHostAddresses())
+ .isEqualTo(makeAddresses("2001:db8::1", "2001:db8::2", "2001:db8::3"));
+
+ verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
+ }
+
+ @Test
+ public void registerHost_nsdManagerThrows_serviceRegistrationFails() throws Exception {
+ prepareTest();
+
+ doThrow(new IllegalArgumentException("NsdManager fails"))
+ .when(mMockNsdManager)
+ .registerService(any(), anyInt(), any(Executor.class), any());
+
+ mNsdPublisher.registerHost(
+ "MyHost",
+ List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ verify(mRegistrationReceiver, times(1)).onError(FAILURE_INTERNAL_ERROR);
+ }
+
+ @Test
+ public void unregisterHost_nsdManagerSucceeds_serviceUnregistrationSucceeds() throws Exception {
+ prepareTest();
+
+ mNsdPublisher.registerHost(
+ "MyHost",
+ List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+ ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+ verify(mMockNsdManager, times(1))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(Executor.class),
+ actualRegistrationListenerCaptor.capture());
+
+ NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+ NsdManager.RegistrationListener actualRegistrationListener =
+ actualRegistrationListenerCaptor.getValue();
+
+ actualRegistrationListener.onServiceRegistered(actualServiceInfo);
+ mNsdPublisher.unregister(mUnregistrationReceiver, 16 /* listenerId */);
+ mTestLooper.dispatchAll();
+ verify(mMockNsdManager, times(1)).unregisterService(actualRegistrationListener);
+
+ actualRegistrationListener.onServiceUnregistered(actualServiceInfo);
+ mTestLooper.dispatchAll();
+ verify(mUnregistrationReceiver, times(1)).onSuccess();
+ }
+
+ @Test
+ public void unregisterHost_nsdManagerFails_serviceUnregistrationFails() throws Exception {
+ prepareTest();
+
+ mNsdPublisher.registerHost(
+ "MyHost",
+ List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+ ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+ verify(mMockNsdManager, times(1))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(Executor.class),
+ actualRegistrationListenerCaptor.capture());
+
+ NsdServiceInfo actualServiceInfo = actualServiceInfoCaptor.getValue();
+ NsdManager.RegistrationListener actualRegistrationListener =
+ actualRegistrationListenerCaptor.getValue();
+
+ actualRegistrationListener.onServiceRegistered(actualServiceInfo);
+ mNsdPublisher.unregister(mUnregistrationReceiver, 16 /* listenerId */);
+ mTestLooper.dispatchAll();
+ verify(mMockNsdManager, times(1)).unregisterService(actualRegistrationListener);
+
+ actualRegistrationListener.onUnregistrationFailed(
+ actualServiceInfo, FAILURE_INTERNAL_ERROR);
+ mTestLooper.dispatchAll();
+ verify(mUnregistrationReceiver, times(1)).onError(0);
+ }
+
+ @Test
+ public void discoverService_serviceDiscovered() throws Exception {
+ prepareTest();
+
+ mNsdPublisher.discoverService("_test._tcp", mDiscoverServiceCallback, 10 /* listenerId */);
+ mTestLooper.dispatchAll();
+ ArgumentCaptor<NsdManager.DiscoveryListener> discoveryListenerArgumentCaptor =
+ ArgumentCaptor.forClass(NsdManager.DiscoveryListener.class);
+ verify(mMockNsdManager, times(1))
+ .discoverServices(
+ eq(new DiscoveryRequest.Builder(PROTOCOL_DNS_SD, "_test._tcp").build()),
+ any(Executor.class),
+ discoveryListenerArgumentCaptor.capture());
+ NsdManager.DiscoveryListener actualDiscoveryListener =
+ discoveryListenerArgumentCaptor.getValue();
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+ serviceInfo.setServiceName("test");
+ serviceInfo.setServiceType(null);
+ actualDiscoveryListener.onServiceFound(serviceInfo);
+ mTestLooper.dispatchAll();
+
+ verify(mDiscoverServiceCallback, times(1))
+ .onServiceDiscovered("test", "_test._tcp", true /* isFound */);
+ }
+
+ @Test
+ public void discoverService_serviceLost() throws Exception {
+ prepareTest();
+
+ mNsdPublisher.discoverService("_test._tcp", mDiscoverServiceCallback, 10 /* listenerId */);
+ mTestLooper.dispatchAll();
+ ArgumentCaptor<NsdManager.DiscoveryListener> discoveryListenerArgumentCaptor =
+ ArgumentCaptor.forClass(NsdManager.DiscoveryListener.class);
+ verify(mMockNsdManager, times(1))
+ .discoverServices(
+ eq(new DiscoveryRequest.Builder(PROTOCOL_DNS_SD, "_test._tcp").build()),
+ any(Executor.class),
+ discoveryListenerArgumentCaptor.capture());
+ NsdManager.DiscoveryListener actualDiscoveryListener =
+ discoveryListenerArgumentCaptor.getValue();
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+ serviceInfo.setServiceName("test");
+ serviceInfo.setServiceType(null);
+ actualDiscoveryListener.onServiceLost(serviceInfo);
+ mTestLooper.dispatchAll();
+
+ verify(mDiscoverServiceCallback, times(1))
+ .onServiceDiscovered("test", "_test._tcp", false /* isFound */);
+ }
+
+ @Test
+ public void stopServiceDiscovery() {
+ prepareTest();
+
+ mNsdPublisher.discoverService("_test._tcp", mDiscoverServiceCallback, 10 /* listenerId */);
+ mTestLooper.dispatchAll();
+ ArgumentCaptor<NsdManager.DiscoveryListener> discoveryListenerArgumentCaptor =
+ ArgumentCaptor.forClass(NsdManager.DiscoveryListener.class);
+ verify(mMockNsdManager, times(1))
+ .discoverServices(
+ eq(new DiscoveryRequest.Builder(PROTOCOL_DNS_SD, "_test._tcp").build()),
+ any(Executor.class),
+ discoveryListenerArgumentCaptor.capture());
+ NsdManager.DiscoveryListener actualDiscoveryListener =
+ discoveryListenerArgumentCaptor.getValue();
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+ serviceInfo.setServiceName("test");
+ serviceInfo.setServiceType(null);
+ actualDiscoveryListener.onServiceFound(serviceInfo);
+ mNsdPublisher.stopServiceDiscovery(10 /* listenerId */);
+ mTestLooper.dispatchAll();
+
+ verify(mMockNsdManager, times(1)).stopServiceDiscovery(actualDiscoveryListener);
+ }
+
+ @Test
+ public void resolveService_serviceResolved() throws Exception {
+ prepareTest();
+
+ mNsdPublisher.resolveService(
+ "test", "_test._tcp", mResolveServiceCallback, 10 /* listenerId */);
+ mTestLooper.dispatchAll();
+ ArgumentCaptor<NsdServiceInfo> serviceInfoArgumentCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.ServiceInfoCallback> serviceInfoCallbackArgumentCaptor =
+ ArgumentCaptor.forClass(NsdManager.ServiceInfoCallback.class);
+ verify(mMockNsdManager, times(1))
+ .registerServiceInfoCallback(
+ serviceInfoArgumentCaptor.capture(),
+ any(Executor.class),
+ serviceInfoCallbackArgumentCaptor.capture());
+ assertThat(serviceInfoArgumentCaptor.getValue().getServiceName()).isEqualTo("test");
+ assertThat(serviceInfoArgumentCaptor.getValue().getServiceType()).isEqualTo("_test._tcp");
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+ serviceInfo.setServiceName("test");
+ serviceInfo.setServiceType("_test._tcp");
+ serviceInfo.setPort(12345);
+ serviceInfo.setHostname("test-host");
+ serviceInfo.setHostAddresses(
+ List.of(
+ InetAddress.parseNumericAddress("2001::1"),
+ InetAddress.parseNumericAddress("2001::2")));
+ serviceInfo.setAttribute("key1", new byte[] {(byte) 0x01, (byte) 0x02});
+ serviceInfo.setAttribute("key2", new byte[] {(byte) 0x03});
+ serviceInfoCallbackArgumentCaptor.getValue().onServiceUpdated(serviceInfo);
+ mTestLooper.dispatchAll();
+
+ verify(mResolveServiceCallback, times(1))
+ .onServiceResolved(
+ eq("test-host"),
+ eq("test"),
+ eq("_test._tcp"),
+ eq(12345),
+ eq(List.of("2001::1", "2001::2")),
+ argThat(
+ new TxtMatcher(
+ List.of(
+ makeTxtAttribute("key1", List.of(0x01, 0x02)),
+ makeTxtAttribute("key2", List.of(0x03))))),
+ anyInt());
+ }
+
+ @Test
+ public void stopServiceResolution() throws Exception {
+ prepareTest();
+
+ mNsdPublisher.resolveService(
+ "test", "_test._tcp", mResolveServiceCallback, 10 /* listenerId */);
+ mTestLooper.dispatchAll();
+ ArgumentCaptor<NsdServiceInfo> serviceInfoArgumentCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.ServiceInfoCallback> serviceInfoCallbackArgumentCaptor =
+ ArgumentCaptor.forClass(NsdManager.ServiceInfoCallback.class);
+ verify(mMockNsdManager, times(1))
+ .registerServiceInfoCallback(
+ serviceInfoArgumentCaptor.capture(),
+ any(Executor.class),
+ serviceInfoCallbackArgumentCaptor.capture());
+ assertThat(serviceInfoArgumentCaptor.getValue().getServiceName()).isEqualTo("test");
+ assertThat(serviceInfoArgumentCaptor.getValue().getServiceType()).isEqualTo("_test._tcp");
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+ serviceInfo.setServiceName("test");
+ serviceInfo.setServiceType("_test._tcp");
+ serviceInfo.setPort(12345);
+ serviceInfo.setHostname("test-host");
+ serviceInfo.setHostAddresses(
+ List.of(
+ InetAddress.parseNumericAddress("2001::1"),
+ InetAddress.parseNumericAddress("2001::2")));
+ serviceInfo.setAttribute("key1", new byte[] {(byte) 0x01, (byte) 0x02});
+ serviceInfo.setAttribute("key2", new byte[] {(byte) 0x03});
+ serviceInfoCallbackArgumentCaptor.getValue().onServiceUpdated(serviceInfo);
+ mNsdPublisher.stopServiceResolution(10 /* listenerId */);
+ mTestLooper.dispatchAll();
+
+ verify(mMockNsdManager, times(1))
+ .unregisterServiceInfoCallback(serviceInfoCallbackArgumentCaptor.getValue());
+ }
+
+ @Test
+ public void reset_unregisterAll() {
+ prepareTest();
+
+ DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
+ DnsTxtAttribute txt2 = makeTxtAttribute("key2", List.of(0x03));
+
+ ArgumentCaptor<NsdServiceInfo> actualServiceInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ ArgumentCaptor<NsdManager.RegistrationListener> actualRegistrationListenerCaptor =
+ ArgumentCaptor.forClass(NsdManager.RegistrationListener.class);
+
+ mNsdPublisher.registerService(
+ null,
+ "MyService",
+ "_test._tcp",
+ List.of("_subtype1", "_subtype2"),
+ 12345,
+ List.of(txt1, txt2),
+ mRegistrationReceiver,
+ 16 /* listenerId */);
+ mTestLooper.dispatchAll();
+
+ verify(mMockNsdManager, times(1))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(Executor.class),
+ actualRegistrationListenerCaptor.capture());
+ NsdManager.RegistrationListener actualListener1 =
+ actualRegistrationListenerCaptor.getValue();
+ actualListener1.onServiceRegistered(actualServiceInfoCaptor.getValue());
+
+ mNsdPublisher.registerService(
+ null,
+ "MyService2",
+ "_test._udp",
+ Collections.emptyList(),
+ 11111,
+ Collections.emptyList(),
+ mRegistrationReceiver,
+ 17 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ verify(mMockNsdManager, times(2))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(Executor.class),
+ actualRegistrationListenerCaptor.capture());
+ NsdManager.RegistrationListener actualListener2 =
+ actualRegistrationListenerCaptor.getAllValues().get(1);
+ actualListener2.onServiceRegistered(actualServiceInfoCaptor.getValue());
+
+ mNsdPublisher.registerHost(
+ "Myhost",
+ List.of("2001:db8::1", "2001:db8::2", "2001:db8::3"),
+ mRegistrationReceiver,
+ 18 /* listenerId */);
+
+ mTestLooper.dispatchAll();
+
+ verify(mMockNsdManager, times(3))
+ .registerService(
+ actualServiceInfoCaptor.capture(),
+ eq(PROTOCOL_DNS_SD),
+ any(Executor.class),
+ actualRegistrationListenerCaptor.capture());
+ NsdManager.RegistrationListener actualListener3 =
+ actualRegistrationListenerCaptor.getAllValues().get(1);
+ actualListener3.onServiceRegistered(actualServiceInfoCaptor.getValue());
+
+ mNsdPublisher.reset();
+ mTestLooper.dispatchAll();
+
+ verify(mMockNsdManager, times(1)).unregisterService(actualListener1);
+ verify(mMockNsdManager, times(1)).unregisterService(actualListener2);
+ verify(mMockNsdManager, times(1)).unregisterService(actualListener3);
+ }
+
+ @Test
+ public void onOtDaemonDied_resetIsCalled() {
+ prepareTest();
+ NsdPublisher spyNsdPublisher = spy(mNsdPublisher);
+
+ spyNsdPublisher.onOtDaemonDied();
+ mTestLooper.dispatchAll();
+
+ verify(spyNsdPublisher, times(1)).reset();
+ }
+
+ private static DnsTxtAttribute makeTxtAttribute(String name, List<Integer> value) {
+ DnsTxtAttribute txtAttribute = new DnsTxtAttribute();
+
+ txtAttribute.name = name;
+ txtAttribute.value = new byte[value.size()];
+
+ for (int i = 0; i < value.size(); ++i) {
+ txtAttribute.value[i] = value.get(i).byteValue();
+ }
+
+ return txtAttribute;
+ }
+
+ private static List<InetAddress> makeAddresses(String... addressStrings) {
+ List<InetAddress> addresses = new ArrayList<>();
+
+ for (String addressString : addressStrings) {
+ addresses.add(InetAddresses.parseNumericAddress(addressString));
+ }
+ return addresses;
+ }
+
+ private static class TxtMatcher implements ArgumentMatcher<List<DnsTxtAttribute>> {
+ private final List<DnsTxtAttribute> mAttributes;
+
+ TxtMatcher(List<DnsTxtAttribute> attributes) {
+ mAttributes = attributes;
+ }
+
+ @Override
+ public boolean matches(List<DnsTxtAttribute> argument) {
+ if (argument.size() != mAttributes.size()) {
+ return false;
+ }
+ for (int i = 0; i < argument.size(); ++i) {
+ if (!Objects.equals(argument.get(i).name, mAttributes.get(i).name)) {
+ return false;
+ }
+ if (!Arrays.equals(argument.get(i).value, mAttributes.get(i).value)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ // @Before and @Test run in different threads. NsdPublisher requires the jobs are run on the
+ // thread looper, so TestLooper needs to be created inside each test case to install the
+ // correct looper.
+ private void prepareTest() {
+ mTestLooper = new TestLooper();
+ Handler handler = new Handler(mTestLooper.getLooper());
+ mNsdPublisher = new NsdPublisher(mMockNsdManager, handler);
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index 44a8ab7..493058f 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -16,45 +16,78 @@
package com.android.server.thread;
+import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
+import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
+import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
+import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
+import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
-import static com.android.testutils.TestPermissionUtil.runAsShell;
+import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.NetworkAgent;
import android.net.NetworkProvider;
import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IActiveOperationalDatasetReceiver;
import android.net.thread.IOperationReceiver;
+import android.net.thread.ThreadNetworkException;
import android.os.Handler;
+import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
+import android.os.UserManager;
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.connectivity.resources.R;
+import com.android.server.connectivity.ConnectivityResources;
+import com.android.server.thread.openthread.MeshcopTxtAttributes;
import com.android.server.thread.openthread.testing.FakeOtDaemon;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InOrder;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
+
/** Unit tests for {@link ThreadNetworkControllerService}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -79,45 +112,88 @@
+ "B9D351B40C0402A0FFF8");
private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_ACTIVE_DATASET_TLVS);
+ private static final String DEFAULT_NETWORK_NAME = "thread-wpan0";
+ private static final int OT_ERROR_NONE = 0;
+ private static final int DEFAULT_SUPPORTED_CHANNEL_MASK = 0x07FFF800; // from channel 11 to 26
+
+ // The DEFAULT_PREFERRED_CHANNEL_MASK is the ot-daemon preferred channel mask. Channel 25 and
+ // 26 are not preferred by dataset. The ThreadNetworkControllerService will only select channel
+ // 11 when it creates randomized dataset.
+ private static final int DEFAULT_PREFERRED_CHANNEL_MASK = 0x06000800; // channel 11, 25 and 26
+ private static final int DEFAULT_SELECTED_CHANNEL = 11;
+ private static final byte[] DEFAULT_SUPPORTED_CHANNEL_MASK_ARRAY = base16().decode("001FFFE0");
+
+ private static final String TEST_VENDOR_OUI = "AC-DE-48";
+ private static final byte[] TEST_VENDOR_OUI_BYTES = new byte[] {(byte) 0xAC, (byte) 0xDE, 0x48};
+ private static final String TEST_VENDOR_NAME = "test vendor";
+ private static final String TEST_MODEL_NAME = "test model";
@Mock private ConnectivityManager mMockConnectivityManager;
@Mock private NetworkAgent mMockNetworkAgent;
@Mock private TunInterfaceController mMockTunIfController;
@Mock private ParcelFileDescriptor mMockTunFd;
@Mock private InfraInterfaceController mMockInfraIfController;
+ @Mock private ThreadPersistentSettings mMockPersistentSettings;
+ @Mock private NsdPublisher mMockNsdPublisher;
+ @Mock private UserManager mMockUserManager;
+ @Mock private IBinder mIBinder;
+ @Mock Resources mResources;
+ @Mock ConnectivityResources mConnectivityResources;
+
private Context mContext;
private TestLooper mTestLooper;
private FakeOtDaemon mFakeOtDaemon;
private ThreadNetworkControllerService mService;
+ @Captor private ArgumentCaptor<ActiveOperationalDataset> mActiveDatasetCaptor;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mContext = ApplicationProvider.getApplicationContext();
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ doNothing()
+ .when(mContext)
+ .enforceCallingOrSelfPermission(
+ eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), anyString());
+
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);
+ when(mMockPersistentSettings.get(any())).thenReturn(true);
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+
+ when(mConnectivityResources.get()).thenReturn(mResources);
+ when(mResources.getString(eq(R.string.config_thread_vendor_name)))
+ .thenReturn(TEST_VENDOR_NAME);
+ when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
+ .thenReturn(TEST_VENDOR_OUI);
+ when(mResources.getString(eq(R.string.config_thread_model_name)))
+ .thenReturn(TEST_MODEL_NAME);
+
mService =
new ThreadNetworkControllerService(
- ApplicationProvider.getApplicationContext(),
+ mContext,
handler,
networkProvider,
() -> mFakeOtDaemon,
mMockConnectivityManager,
mMockTunIfController,
- mMockInfraIfController);
+ mMockInfraIfController,
+ mMockPersistentSettings,
+ mMockNsdPublisher,
+ mMockUserManager,
+ mConnectivityResources,
+ () -> DEFAULT_COUNTRY_CODE);
mService.setTestNetworkAgent(mMockNetworkAgent);
}
@Test
- public void initialize_tunInterfaceSetToOtDaemon() throws Exception {
+ public void initialize_tunInterfaceAndNsdPublisherSetToOtDaemon() throws Exception {
when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
mService.initialize();
@@ -125,6 +201,94 @@
verify(mMockTunIfController, times(1)).createTunInterface();
assertThat(mFakeOtDaemon.getTunFd()).isEqualTo(mMockTunFd);
+ assertThat(mFakeOtDaemon.getNsdPublisher()).isEqualTo(mMockNsdPublisher);
+ }
+
+ @Test
+ public void initialize_vendorAndModelNameInResourcesAreSetToOtDaemon() throws Exception {
+ when(mResources.getString(eq(R.string.config_thread_vendor_name)))
+ .thenReturn(TEST_VENDOR_NAME);
+ when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
+ .thenReturn(TEST_VENDOR_OUI);
+ when(mResources.getString(eq(R.string.config_thread_model_name)))
+ .thenReturn(TEST_MODEL_NAME);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ MeshcopTxtAttributes meshcopTxts = mFakeOtDaemon.getOverriddenMeshcopTxtAttributes();
+ assertThat(meshcopTxts.vendorName).isEqualTo(TEST_VENDOR_NAME);
+ assertThat(meshcopTxts.vendorOui).isEqualTo(TEST_VENDOR_OUI_BYTES);
+ assertThat(meshcopTxts.modelName).isEqualTo(TEST_MODEL_NAME);
+ }
+
+ @Test
+ public void getMeshcopTxtAttributes_emptyVendorName_accepted() {
+ when(mResources.getString(eq(R.string.config_thread_vendor_name))).thenReturn("");
+
+ MeshcopTxtAttributes meshcopTxts =
+ ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources);
+
+ assertThat(meshcopTxts.vendorName).isEqualTo("");
+ }
+
+ @Test
+ public void getMeshcopTxtAttributes_tooLongVendorName_throwsIllegalStateException() {
+ when(mResources.getString(eq(R.string.config_thread_vendor_name)))
+ .thenReturn("vendor name is 25 bytes!!");
+
+ assertThrows(
+ IllegalStateException.class,
+ () -> ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources));
+ }
+
+ @Test
+ public void getMeshcopTxtAttributes_tooLongModelName_throwsIllegalStateException() {
+ when(mResources.getString(eq(R.string.config_thread_model_name)))
+ .thenReturn("model name is 25 bytes!!!");
+
+ assertThrows(
+ IllegalStateException.class,
+ () -> ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources));
+ }
+
+ @Test
+ public void getMeshcopTxtAttributes_emptyModelName_accepted() {
+ when(mResources.getString(eq(R.string.config_thread_model_name))).thenReturn("");
+
+ var meshcopTxts = ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources);
+ assertThat(meshcopTxts.modelName).isEqualTo("");
+ }
+
+ @Test
+ public void getMeshcopTxtAttributes_invalidVendorOui_throwsIllegalStateException() {
+ assertThrows(
+ IllegalStateException.class, () -> getMeshcopTxtAttributesWithVendorOui("ABCDEFA"));
+ assertThrows(
+ IllegalStateException.class, () -> getMeshcopTxtAttributesWithVendorOui("ABCDEG"));
+ assertThrows(
+ IllegalStateException.class, () -> getMeshcopTxtAttributesWithVendorOui("ABCD"));
+ assertThrows(
+ IllegalStateException.class,
+ () -> getMeshcopTxtAttributesWithVendorOui("AB.CD.EF"));
+ }
+
+ @Test
+ public void getMeshcopTxtAttributes_validVendorOui_accepted() {
+ assertThat(getMeshcopTxtAttributesWithVendorOui("010203")).isEqualTo(new byte[] {1, 2, 3});
+ assertThat(getMeshcopTxtAttributesWithVendorOui("01-02-03"))
+ .isEqualTo(new byte[] {1, 2, 3});
+ assertThat(getMeshcopTxtAttributesWithVendorOui("01:02:03"))
+ .isEqualTo(new byte[] {1, 2, 3});
+ assertThat(getMeshcopTxtAttributesWithVendorOui("ABCDEF"))
+ .isEqualTo(new byte[] {(byte) 0xAB, (byte) 0xCD, (byte) 0xEF});
+ assertThat(getMeshcopTxtAttributesWithVendorOui("abcdef"))
+ .isEqualTo(new byte[] {(byte) 0xAB, (byte) 0xCD, (byte) 0xEF});
+ }
+
+ private byte[] getMeshcopTxtAttributesWithVendorOui(String vendorOui) {
+ when(mResources.getString(eq(R.string.config_thread_vendor_oui))).thenReturn(vendorOui);
+ return ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources).vendorOui;
}
@Test
@@ -133,9 +297,7 @@
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));
+ mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver);
mTestLooper.dispatchAll();
verify(mockReceiver, never()).onSuccess();
@@ -147,9 +309,7 @@
mService.initialize();
final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver));
+ 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.
@@ -160,4 +320,200 @@
verify(mockReceiver, times(1)).onSuccess();
verify(mMockNetworkAgent, times(1)).register();
}
+
+ @Test
+ public void userRestriction_initWithUserRestricted_otDaemonNotStarted() {
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.isInitialized()).isFalse();
+ }
+
+ @Test
+ public void userRestriction_initWithUserNotRestricted_threadIsEnabled() {
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+ }
+
+ @Test
+ public void userRestriction_userBecomesRestricted_stateIsDisabledButNotPersisted() {
+ AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+ doAnswer(
+ invocation -> {
+ receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
+ return null;
+ })
+ .when(mContext)
+ .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+ verify(mMockPersistentSettings, never())
+ .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(false));
+ }
+
+ @Test
+ public void userRestriction_userBecomesNotRestricted_stateIsEnabledButNotPersisted() {
+ AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+ doAnswer(
+ invocation -> {
+ receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
+ return null;
+ })
+ .when(mContext)
+ .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+ verify(mMockPersistentSettings, never())
+ .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(true));
+ }
+
+ @Test
+ public void userRestriction_setEnabledWhenUserRestricted_failedPreconditionError() {
+ when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+ mService.initialize();
+
+ CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
+ mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
+ mTestLooper.dispatchAll();
+
+ var thrown = assertThrows(ExecutionException.class, () -> setEnabledFuture.get());
+ ThreadNetworkException failure = (ThreadNetworkException) thrown.getCause();
+ assertThat(failure.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
+ }
+
+ private static IOperationReceiver newOperationReceiver(CompletableFuture<Void> future) {
+ return new IOperationReceiver.Stub() {
+ @Override
+ public void onSuccess() {
+ future.complete(null);
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ future.completeExceptionally(new ThreadNetworkException(errorCode, errorMessage));
+ }
+ };
+ }
+
+ @Test
+ public void createRandomizedDataset_succeed_activeDatasetCreated() throws Exception {
+ final IActiveOperationalDatasetReceiver mockReceiver =
+ mock(IActiveOperationalDatasetReceiver.class);
+ mFakeOtDaemon.setChannelMasks(
+ DEFAULT_SUPPORTED_CHANNEL_MASK, DEFAULT_PREFERRED_CHANNEL_MASK);
+ mFakeOtDaemon.setChannelMasksReceiverOtError(OT_ERROR_NONE);
+
+ mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, never()).onError(anyInt(), anyString());
+ verify(mockReceiver, times(1)).onSuccess(mActiveDatasetCaptor.capture());
+ ActiveOperationalDataset activeDataset = mActiveDatasetCaptor.getValue();
+ assertThat(activeDataset.getNetworkName()).isEqualTo(DEFAULT_NETWORK_NAME);
+ assertThat(activeDataset.getChannelMask().size()).isEqualTo(1);
+ assertThat(activeDataset.getChannelMask().get(CHANNEL_PAGE_24_GHZ))
+ .isEqualTo(DEFAULT_SUPPORTED_CHANNEL_MASK_ARRAY);
+ assertThat(activeDataset.getChannel()).isEqualTo(DEFAULT_SELECTED_CHANNEL);
+ }
+
+ @Test
+ public void createRandomizedDataset_otDaemonRemoteFailure_returnsPreconditionError()
+ throws Exception {
+ final IActiveOperationalDatasetReceiver mockReceiver =
+ mock(IActiveOperationalDatasetReceiver.class);
+ mFakeOtDaemon.setChannelMasksReceiverOtError(OT_ERROR_INVALID_STATE);
+ when(mockReceiver.asBinder()).thenReturn(mIBinder);
+
+ mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, never()).onSuccess(any(ActiveOperationalDataset.class));
+ verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString());
+ }
+
+ @Test
+ public void forceStopOtDaemonForTest_noPermission_throwsSecurityException() {
+ doThrow(new SecurityException(""))
+ .when(mContext)
+ .enforceCallingOrSelfPermission(eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), any());
+
+ assertThrows(
+ SecurityException.class,
+ () -> mService.forceStopOtDaemonForTest(true, new IOperationReceiver.Default()));
+ }
+
+ @Test
+ public void forceStopOtDaemonForTest_enabled_otDaemonDiesAndJoinFails() throws Exception {
+ mService.initialize();
+ IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+ IOperationReceiver mockJoinReceiver = mock(IOperationReceiver.class);
+
+ mService.forceStopOtDaemonForTest(true, mockReceiver);
+ mTestLooper.dispatchAll();
+ mService.join(DEFAULT_ACTIVE_DATASET, mockJoinReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onSuccess();
+ assertThat(mFakeOtDaemon.isInitialized()).isFalse();
+ verify(mockJoinReceiver, times(1)).onError(eq(ERROR_THREAD_DISABLED), anyString());
+ }
+
+ @Test
+ public void forceStopOtDaemonForTest_disable_otDaemonRestartsAndJoinSccess() throws Exception {
+ mService.initialize();
+ IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+ IOperationReceiver mockJoinReceiver = mock(IOperationReceiver.class);
+
+ mService.forceStopOtDaemonForTest(true, mock(IOperationReceiver.class));
+ mTestLooper.dispatchAll();
+ mService.forceStopOtDaemonForTest(false, mockReceiver);
+ mTestLooper.dispatchAll();
+ mService.join(DEFAULT_ACTIVE_DATASET, mockJoinReceiver);
+ mTestLooper.dispatchAll();
+ mTestLooper.moveTimeForward(FakeOtDaemon.JOIN_DELAY.toMillis() + 100);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onSuccess();
+ assertThat(mFakeOtDaemon.isInitialized()).isTrue();
+ verify(mockJoinReceiver, times(1)).onSuccess();
+ }
+
+ @Test
+ public void onOtDaemonDied_joinedNetwork_interfaceStateBackToUp() throws Exception {
+ mService.initialize();
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+ mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver);
+ mTestLooper.dispatchAll();
+ mTestLooper.moveTimeForward(FakeOtDaemon.JOIN_DELAY.toMillis() + 100);
+ mTestLooper.dispatchAll();
+
+ Mockito.reset(mMockInfraIfController);
+ mFakeOtDaemon.terminate();
+ mTestLooper.dispatchAll();
+
+ verify(mMockTunIfController, times(1)).onOtDaemonDied();
+ InOrder inOrder = Mockito.inOrder(mMockTunIfController);
+ inOrder.verify(mMockTunIfController, times(1)).setInterfaceUp(false);
+ inOrder.verify(mMockTunIfController, times(1)).setInterfaceUp(true);
+ }
}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
index 17cdd01..ca9741d 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
@@ -19,9 +19,11 @@
import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
+import static com.android.server.thread.ThreadPersistentSettings.THREAD_COUNTRY_CODE;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyDouble;
@@ -55,6 +57,7 @@
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -70,6 +73,9 @@
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
@@ -80,6 +86,8 @@
public class ThreadNetworkCountryCodeTest {
private static final String TEST_COUNTRY_CODE_US = "US";
private static final String TEST_COUNTRY_CODE_CN = "CN";
+ private static final String TEST_COUNTRY_CODE_INVALID = "INVALID";
+ private static final String TEST_WIFI_DEFAULT_COUNTRY_CODE = "00";
private static final int TEST_SIM_SLOT_INDEX_0 = 0;
private static final int TEST_SIM_SLOT_INDEX_1 = 1;
@@ -97,6 +105,7 @@
@Mock List<SubscriptionInfo> mSubscriptionInfoList;
@Mock SubscriptionInfo mSubscriptionInfo0;
@Mock SubscriptionInfo mSubscriptionInfo1;
+ @Mock ThreadPersistentSettings mPersistentSettings;
private ThreadNetworkCountryCode mThreadNetworkCountryCode;
private boolean mErrorSetCountryCode;
@@ -144,16 +153,21 @@
.when(mThreadNetworkControllerService)
.setCountryCode(any(), any(IOperationReceiver.class));
- mThreadNetworkCountryCode =
- new ThreadNetworkCountryCode(
- mLocationManager,
- mThreadNetworkControllerService,
- mGeocoder,
- mConnectivityResources,
- mWifiManager,
- mContext,
- mTelephonyManager,
- mSubscriptionManager);
+ mThreadNetworkCountryCode = newCountryCodeWithOemSource(null);
+ }
+
+ private ThreadNetworkCountryCode newCountryCodeWithOemSource(@Nullable String oemCountryCode) {
+ return new ThreadNetworkCountryCode(
+ mLocationManager,
+ mThreadNetworkControllerService,
+ mGeocoder,
+ mConnectivityResources,
+ mWifiManager,
+ mContext,
+ mTelephonyManager,
+ mSubscriptionManager,
+ oemCountryCode,
+ mPersistentSettings);
}
private static Address newAddress(String countryCode) {
@@ -163,6 +177,13 @@
}
@Test
+ public void threadNetworkCountryCode_invalidOemCountryCode_illegalArgumentExceptionIsThrown() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> newCountryCodeWithOemSource(TEST_COUNTRY_CODE_INVALID));
+ }
+
+ @Test
public void initialize_defaultCountryCodeIsUsed() {
mThreadNetworkCountryCode.initialize();
@@ -170,6 +191,15 @@
}
@Test
+ public void initialize_oemCountryCodeAvailable_oemCountryCodeIsUsed() {
+ mThreadNetworkCountryCode = newCountryCodeWithOemSource(TEST_COUNTRY_CODE_US);
+
+ mThreadNetworkCountryCode.initialize();
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+ }
+
+ @Test
public void initialize_locationUseIsDisabled_locationFunctionIsNotCalled() {
when(mResources.getBoolean(R.bool.config_thread_location_use_for_country_code_enabled))
.thenReturn(false);
@@ -233,6 +263,21 @@
}
@Test
+ public void wifiCountryCode_wifiDefaultCountryCodeIsActive_wifiCountryCodeIsNotUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ verify(mWifiManager)
+ .registerActiveCountryCodeChangedCallback(
+ any(), mWifiCountryCodeReceiverCaptor.capture());
+ mWifiCountryCodeReceiverCaptor
+ .getValue()
+ .onActiveCountryCodeChanged(TEST_WIFI_DEFAULT_COUNTRY_CODE);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode())
+ .isNotEqualTo(TEST_WIFI_DEFAULT_COUNTRY_CODE);
+ }
+
+ @Test
public void wifiCountryCode_wifiCountryCodeIsInactive_defaultCountryCodeIsUsed() {
mThreadNetworkCountryCode.initialize();
verify(mWifiManager)
@@ -406,4 +451,30 @@
.setCountryCode(eq(TEST_COUNTRY_CODE_CN), mOperationReceiverCaptor.capture());
assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
}
+
+ @Test
+ public void settingsCountryCode_settingsCountryCodeIsActive_settingsCountryCodeIsUsed() {
+ when(mPersistentSettings.get(THREAD_COUNTRY_CODE)).thenReturn(TEST_COUNTRY_CODE_CN);
+ mThreadNetworkCountryCode.initialize();
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void dump_allCountryCodeInfoAreDumped() {
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(stringWriter);
+
+ mThreadNetworkCountryCode.dump(new FileDescriptor(), printWriter, null);
+ String outputString = stringWriter.toString();
+
+ assertThat(outputString).contains("mOverrideCountryCodeInfo");
+ assertThat(outputString).contains("mTelephonyCountryCodeSlotInfoMap");
+ assertThat(outputString).contains("mTelephonyCountryCodeInfo");
+ assertThat(outputString).contains("mWifiCountryCodeInfo");
+ assertThat(outputString).contains("mTelephonyLastCountryCodeInfo");
+ assertThat(outputString).contains("mLocationCountryCodeInfo");
+ assertThat(outputString).contains("mOemCountryCodeInfo");
+ assertThat(outputString).contains("mCurrentCountryCodeInfo");
+ }
}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
index c7e0eca..9f2d0cb 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
@@ -16,10 +16,15 @@
package com.android.server.thread;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.contains;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.validateMockitoUsage;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -45,19 +50,19 @@
@SmallTest
public class ThreadNetworkShellCommandTest {
private static final String TAG = "ThreadNetworkShellCommandTTest";
- @Mock ThreadNetworkService mThreadNetworkService;
- @Mock ThreadNetworkCountryCode mThreadNetworkCountryCode;
+ @Mock ThreadNetworkControllerService mControllerService;
+ @Mock ThreadNetworkCountryCode mCountryCode;
@Mock PrintWriter mErrorWriter;
@Mock PrintWriter mOutputWriter;
- ThreadNetworkShellCommand mThreadNetworkShellCommand;
+ ThreadNetworkShellCommand mShellCommand;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mThreadNetworkShellCommand = new ThreadNetworkShellCommand(mThreadNetworkCountryCode);
- mThreadNetworkShellCommand.setPrintWriters(mOutputWriter, mErrorWriter);
+ mShellCommand = new ThreadNetworkShellCommand(mControllerService, mCountryCode);
+ mShellCommand.setPrintWriters(mOutputWriter, mErrorWriter);
}
@After
@@ -68,9 +73,9 @@
@Test
public void getCountryCode_executeInUnrootedShell_allowed() {
BinderUtil.setUid(Process.SHELL_UID);
- when(mThreadNetworkCountryCode.getCountryCode()).thenReturn("US");
+ when(mCountryCode.getCountryCode()).thenReturn("US");
- mThreadNetworkShellCommand.exec(
+ mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
@@ -84,14 +89,14 @@
public void forceSetCountryCodeEnabled_executeInUnrootedShell_notAllowed() {
BinderUtil.setUid(Process.SHELL_UID);
- mThreadNetworkShellCommand.exec(
+ mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
new FileDescriptor(),
new String[] {"force-country-code", "enabled", "US"});
- verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(eq("US"));
+ verify(mCountryCode, never()).setOverrideCountryCode(eq("US"));
verify(mErrorWriter).println(contains("force-country-code"));
}
@@ -99,28 +104,28 @@
public void forceSetCountryCodeEnabled_executeInRootedShell_allowed() {
BinderUtil.setUid(Process.ROOT_UID);
- mThreadNetworkShellCommand.exec(
+ mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
new FileDescriptor(),
new String[] {"force-country-code", "enabled", "US"});
- verify(mThreadNetworkCountryCode).setOverrideCountryCode(eq("US"));
+ verify(mCountryCode).setOverrideCountryCode(eq("US"));
}
@Test
public void forceSetCountryCodeDisabled_executeInUnrootedShell_notAllowed() {
BinderUtil.setUid(Process.SHELL_UID);
- mThreadNetworkShellCommand.exec(
+ mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
new FileDescriptor(),
new String[] {"force-country-code", "disabled"});
- verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(any());
+ verify(mCountryCode, never()).setOverrideCountryCode(any());
verify(mErrorWriter).println(contains("force-country-code"));
}
@@ -128,13 +133,64 @@
public void forceSetCountryCodeDisabled_executeInRootedShell_allowed() {
BinderUtil.setUid(Process.ROOT_UID);
- mThreadNetworkShellCommand.exec(
+ mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
new FileDescriptor(),
new String[] {"force-country-code", "disabled"});
- verify(mThreadNetworkCountryCode).clearOverrideCountryCode();
+ verify(mCountryCode).clearOverrideCountryCode();
+ }
+
+ @Test
+ public void forceStopOtDaemon_executeInUnrootedShell_failedAndServiceApiNotCalled() {
+ BinderUtil.setUid(Process.SHELL_UID);
+
+ mShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-stop-ot-daemon", "enabled"});
+
+ verify(mControllerService, never()).forceStopOtDaemonForTest(anyBoolean(), any());
+ verify(mErrorWriter, atLeastOnce()).println(contains("force-stop-ot-daemon"));
+ verify(mOutputWriter, never()).println();
+ }
+
+ @Test
+ public void forceStopOtDaemon_serviceThrows_failed() {
+ BinderUtil.setUid(Process.ROOT_UID);
+ doThrow(new SecurityException(""))
+ .when(mControllerService)
+ .forceStopOtDaemonForTest(eq(true), any());
+
+ mShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-stop-ot-daemon", "enabled"});
+
+ verify(mControllerService, times(1)).forceStopOtDaemonForTest(eq(true), any());
+ verify(mOutputWriter, never()).println();
+ }
+
+ @Test
+ public void forceStopOtDaemon_serviceApiTimeout_failedWithTimeoutError() {
+ BinderUtil.setUid(Process.ROOT_UID);
+ doNothing().when(mControllerService).forceStopOtDaemonForTest(eq(true), any());
+
+ mShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-stop-ot-daemon", "enabled"});
+
+ verify(mControllerService, times(1)).forceStopOtDaemonForTest(eq(true), any());
+ verify(mErrorWriter, atLeastOnce()).println(contains("timeout"));
+ verify(mOutputWriter, never()).println();
}
}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
new file mode 100644
index 0000000..7d2fe91
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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_COUNTRY_CODE;
+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.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.res.Resources;
+import android.os.PersistableBundle;
+import android.util.AtomicFile;
+
+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.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 {
+ private static final String TEST_COUNTRY_CODE = "CN";
+
+ @Mock private AtomicFile mAtomicFile;
+ @Mock Resources mResources;
+ @Mock ConnectivityResources mConnectivityResources;
+
+ private ThreadPersistentSettings mThreadPersistentSettings;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ when(mConnectivityResources.get()).thenReturn(mResources);
+ when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
+
+ FileOutputStream fos = mock(FileOutputStream.class);
+ when(mAtomicFile.startWrite()).thenReturn(fos);
+ mThreadPersistentSettings =
+ new ThreadPersistentSettings(mAtomicFile, mConnectivityResources);
+ }
+
+ /** Called after each test */
+ @After
+ public void tearDown() {
+ validateMockitoUsage();
+ }
+
+ @Test
+ public void initialize_readsFromFile() throws Exception {
+ byte[] data = createXmlForParsing(THREAD_ENABLED.key, false);
+ setupAtomicFileMockForRead(data);
+
+ mThreadPersistentSettings.initialize();
+
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+ }
+
+ @Test
+ public void initialize_ThreadDisabledInResources_returnsThreadDisabled() throws Exception {
+ when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
+ setupAtomicFileMockForRead(new byte[0]);
+
+ mThreadPersistentSettings.initialize();
+
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+ }
+
+ @Test
+ public void initialize_ThreadDisabledInResourcesButEnabledInXml_returnsThreadEnabled()
+ throws Exception {
+ when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
+ byte[] data = createXmlForParsing(THREAD_ENABLED.key, true);
+ setupAtomicFileMockForRead(data);
+
+ mThreadPersistentSettings.initialize();
+
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isTrue();
+ }
+
+ @Test
+ public void put_ThreadFeatureEnabledTrue_returnsTrue() throws Exception {
+ mThreadPersistentSettings.put(THREAD_ENABLED.key, true);
+
+ assertThat(mThreadPersistentSettings.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 {
+ mThreadPersistentSettings.put(THREAD_ENABLED.key, false);
+
+ assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+ // Confirm that file writes have been triggered.
+ verify(mAtomicFile).startWrite();
+ verify(mAtomicFile).finishWrite(any());
+ }
+
+ @Test
+ public void put_ThreadCountryCodeString_returnsString() throws Exception {
+ mThreadPersistentSettings.put(THREAD_COUNTRY_CODE.key, TEST_COUNTRY_CODE);
+
+ assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isEqualTo(TEST_COUNTRY_CODE);
+
+ // Confirm that file writes have been triggered.
+ verify(mAtomicFile).startWrite();
+ verify(mAtomicFile).finishWrite(any());
+ }
+
+ @Test
+ public void put_ThreadCountryCodeNull_returnsNull() throws Exception {
+ mThreadPersistentSettings.put(THREAD_COUNTRY_CODE.key, null);
+
+ assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isNull();
+
+ // Confirm that file writes have been triggered.
+ verify(mAtomicFile).startWrite();
+ verify(mAtomicFile).finishWrite(any());
+ }
+
+ 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/utils/Android.bp b/thread/tests/utils/Android.bp
new file mode 100644
index 0000000..726ec9d
--- /dev/null
+++ b/thread/tests/utils/Android.bp
@@ -0,0 +1,38 @@
+//
+// 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_team: "trendy_team_fwk_thread_network",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "ThreadNetworkTestUtils",
+ min_sdk_version: "30",
+ static_libs: [
+ "compatibility-device-util-axt",
+ "net-tests-utils",
+ "net-utils-device-common",
+ "net-utils-device-common-bpf",
+ "net-utils-device-common-struct-base",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+ defaults: [
+ "framework-connectivity-test-defaults",
+ ],
+}
diff --git a/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
new file mode 100644
index 0000000..b586a19
--- /dev/null
+++ b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread.utils;
+
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import static com.android.testutils.RecorderCallback.CallbackEntry.LINK_PROPERTIES_CHANGED;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.TestNetworkSpecifier;
+import android.os.Looper;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.testutils.TestableNetworkAgent;
+import com.android.testutils.TestableNetworkCallback;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** A class that can create/destroy a test network based on TAP interface. */
+public final class TapTestNetworkTracker {
+ private static final Duration TIMEOUT = Duration.ofSeconds(2);
+ private final Context mContext;
+ private final Looper mLooper;
+ private TestNetworkInterface mInterface;
+ private TestableNetworkAgent mAgent;
+ private Network mNetwork;
+ private final TestableNetworkCallback mNetworkCallback;
+ private final ConnectivityManager mConnectivityManager;
+
+ /**
+ * Constructs a {@link TapTestNetworkTracker}.
+ *
+ * <p>It creates a TAP interface (e.g. testtap0) and registers a test network using that
+ * interface. It also requests the test network by {@link ConnectivityManager#requestNetwork} so
+ * the test network won't be automatically turned down by {@link
+ * com.android.server.ConnectivityService}.
+ */
+ public TapTestNetworkTracker(Context context, Looper looper) {
+ mContext = context;
+ mLooper = looper;
+ mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
+ mNetworkCallback = new TestableNetworkCallback();
+ runAsShell(MANAGE_TEST_NETWORKS, this::setUpTestNetwork);
+ }
+
+ /** Tears down the test network. */
+ public void tearDown() {
+ runAsShell(MANAGE_TEST_NETWORKS, this::tearDownTestNetwork);
+ }
+
+ /** Returns the interface name of the test network. */
+ public String getInterfaceName() {
+ return mInterface.getInterfaceName();
+ }
+
+ /** Returns the {@link android.net.Network} of the test network. */
+ public Network getNetwork() {
+ return mNetwork;
+ }
+
+ private void setUpTestNetwork() throws Exception {
+ mInterface = mContext.getSystemService(TestNetworkManager.class).createTapInterface();
+
+ mConnectivityManager.requestNetwork(newNetworkRequest(), mNetworkCallback);
+
+ LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(getInterfaceName());
+ mAgent =
+ new TestableNetworkAgent(
+ mContext,
+ mLooper,
+ newNetworkCapabilities(),
+ lp,
+ new NetworkAgentConfig.Builder().build());
+ mNetwork = mAgent.register();
+ mAgent.markConnected();
+
+ PollingCheck.check(
+ "No usable address on interface",
+ TIMEOUT.toMillis(),
+ () -> hasUsableAddress(mNetwork, getInterfaceName()));
+
+ lp.setLinkAddresses(makeLinkAddresses());
+ mAgent.sendLinkProperties(lp);
+ mNetworkCallback.eventuallyExpect(
+ LINK_PROPERTIES_CHANGED,
+ TIMEOUT.toMillis(),
+ l -> !l.getLp().getAddresses().isEmpty());
+ }
+
+ private void tearDownTestNetwork() throws IOException {
+ mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
+ mAgent.unregister();
+ mInterface.getFileDescriptor().close();
+ mAgent.waitForIdle(TIMEOUT.toMillis());
+ }
+
+ private NetworkRequest newNetworkRequest() {
+ return new NetworkRequest.Builder()
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName()))
+ .build();
+ }
+
+ private NetworkCapabilities newNetworkCapabilities() {
+ return new NetworkCapabilities()
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName()));
+ }
+
+ private List<LinkAddress> makeLinkAddresses() {
+ List<LinkAddress> linkAddresses = new ArrayList<>();
+ List<InterfaceAddress> interfaceAddresses = Collections.emptyList();
+
+ try {
+ interfaceAddresses =
+ NetworkInterface.getByName(getInterfaceName()).getInterfaceAddresses();
+ } catch (SocketException ignored) {
+ // Ignore failures when getting the addresses.
+ }
+
+ for (InterfaceAddress address : interfaceAddresses) {
+ linkAddresses.add(
+ new LinkAddress(address.getAddress(), address.getNetworkPrefixLength()));
+ }
+
+ return linkAddresses;
+ }
+
+ private static boolean hasUsableAddress(Network network, String interfaceName) {
+ try {
+ if (NetworkInterface.getByName(interfaceName).getInterfaceAddresses().isEmpty()) {
+ return false;
+ }
+ } catch (SocketException e) {
+ return false;
+ }
+ // Check if the link-local address can be used. Address flags are not available without
+ // elevated permissions, so check that bindSocket works.
+ try {
+ FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
+ network.bindSocket(sock);
+ Os.connect(sock, parseNumericAddress("ff02::fb%" + interfaceName), 12345);
+ Os.close(sock);
+ } catch (ErrnoException | IOException e) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/thread/tests/utils/src/android/net/thread/utils/ThreadFeatureCheckerRule.java b/thread/tests/utils/src/android/net/thread/utils/ThreadFeatureCheckerRule.java
new file mode 100644
index 0000000..bee9ceb
--- /dev/null
+++ b/thread/tests/utils/src/android/net/thread/utils/ThreadFeatureCheckerRule.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread.utils;
+
+import static com.android.testutils.DeviceInfoUtils.isKernelVersionAtLeast;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.net.thread.ThreadNetworkManager;
+import android.os.SystemProperties;
+import android.os.VintfRuntimeInfo;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A rule used to skip Thread tests when the device doesn't support a specific feature indicated by
+ * {@code ThreadFeatureCheckerRule.Requires*}.
+ */
+public final class ThreadFeatureCheckerRule implements TestRule {
+ private static final String KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED = "5.15.0";
+ private static final int KERNEL_ANDROID_VERSION_MULTICAST_ROUTING_SUPPORTED = 14;
+
+ /**
+ * Annotates a test class or method requires the Thread feature to run.
+ *
+ * <p>In Absence of the Thread feature, the test class or method will be ignored.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.METHOD, ElementType.TYPE})
+ public @interface RequiresThreadFeature {}
+
+ /**
+ * Annotates a test class or method requires the kernel IPv6 multicast routing feature to run.
+ *
+ * <p>In Absence of the multicast routing feature, the test class or method will be ignored.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.METHOD, ElementType.TYPE})
+ public @interface RequiresIpv6MulticastRouting {}
+
+ /**
+ * Annotates a test class or method requires the simulation Thread device (i.e. ot-cli-ftd) to
+ * run.
+ *
+ * <p>In Absence of the simulation device, the test class or method will be ignored.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.METHOD, ElementType.TYPE})
+ public @interface RequiresSimulationThreadDevice {}
+
+ @Override
+ public Statement apply(final Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ if (hasAnnotation(RequiresThreadFeature.class, description)) {
+ assumeTrue(
+ "Skipping test because the Thread feature is unavailable",
+ hasThreadFeature());
+ }
+
+ if (hasAnnotation(RequiresIpv6MulticastRouting.class, description)) {
+ assumeTrue(
+ "Skipping test because kernel IPv6 multicast routing is unavailable",
+ hasIpv6MulticastRouting());
+ }
+
+ if (hasAnnotation(RequiresSimulationThreadDevice.class, description)) {
+ assumeTrue(
+ "Skipping test because simulation Thread device is unavailable",
+ hasSimulationThreadDevice());
+ }
+
+ base.evaluate();
+ }
+ };
+ }
+
+ /** Returns {@code true} if a test method or the test class is annotated with annotation. */
+ private <T extends Annotation> boolean hasAnnotation(
+ Class<T> annotationClass, Description description) {
+ // Method annotation
+ boolean hasAnnotation = description.getAnnotation(annotationClass) != null;
+
+ // Class annotation
+ Class<?> clazz = description.getTestClass();
+ while (!hasAnnotation && clazz != Object.class) {
+ hasAnnotation |= clazz.getAnnotation(annotationClass) != null;
+ clazz = clazz.getSuperclass();
+ }
+
+ return hasAnnotation;
+ }
+
+ /** Returns {@code true} if this device has the Thread feature supported. */
+ private static boolean hasThreadFeature() {
+ final Context context = ApplicationProvider.getApplicationContext();
+ return context.getSystemService(ThreadNetworkManager.class) != null;
+ }
+
+ /**
+ * Returns {@code true} if this device has the kernel IPv6 multicast routing feature enabled.
+ */
+ private static boolean hasIpv6MulticastRouting() {
+ // The kernel IPv6 multicast routing (i.e. IPV6_MROUTE) is enabled on kernel version
+ // android14-5.15.0 and later
+ return isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED)
+ && isKernelAndroidVersionAtLeast(
+ KERNEL_ANDROID_VERSION_MULTICAST_ROUTING_SUPPORTED);
+ }
+
+ /**
+ * Returns {@code true} if the android version in the kernel version of this device is equal to
+ * or larger than the given {@code minVersion}.
+ */
+ private static boolean isKernelAndroidVersionAtLeast(int minVersion) {
+ final String osRelease = VintfRuntimeInfo.getOsRelease();
+ final Pattern pattern = Pattern.compile("android(\\d+)");
+ Matcher matcher = pattern.matcher(osRelease);
+
+ if (matcher.find()) {
+ int version = Integer.parseInt(matcher.group(1));
+ return (version >= minVersion);
+ }
+ return false;
+ }
+
+ /** Returns {@code true} if the simulation Thread device is supported. */
+ private static boolean hasSimulationThreadDevice() {
+ // Simulation radio is supported on only Cuttlefish
+ return SystemProperties.get("ro.product.model").startsWith("Cuttlefish");
+ }
+}
diff --git a/tools/Android.bp b/tools/Android.bp
index 3ce76f6..2c2ed14 100644
--- a/tools/Android.bp
+++ b/tools/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_fwk_core_networking",
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -41,6 +42,7 @@
name: "jarjar-rules-generator-testjavalib",
srcs: ["testdata/java/**/*.java"],
libs: ["unsupportedappusage"],
+ sdk_version: "core_platform",
visibility: ["//visibility:private"],
}
@@ -55,6 +57,7 @@
static_libs: [
"framework-connectivity.stubs.module_lib",
],
+ sdk_version: "module_current",
// Not strictly necessary but specified as this MUST not have generate
// a dex jar as that will break the tests.
compile_dex: false,
@@ -66,6 +69,7 @@
static_libs: [
"framework-connectivity-t.stubs.module_lib",
],
+ sdk_version: "module_current",
// Not strictly necessary but specified as this MUST not have generate
// a dex jar as that will break the tests.
compile_dex: false,
@@ -79,6 +83,8 @@
],
data: [
"testdata/test-jarjar-excludes.txt",
+ // txt with Test classes to test they aren't included when added to jarjar excludes
+ "testdata/test-jarjar-excludes-testclass.txt",
// two unsupportedappusage lists with different classes to test using multiple lists
"testdata/test-unsupportedappusage.txt",
"testdata/test-other-unsupportedappusage.txt",
diff --git a/tools/gen_jarjar_test.py b/tools/gen_jarjar_test.py
index f5bf499..12038e9 100644
--- a/tools/gen_jarjar_test.py
+++ b/tools/gen_jarjar_test.py
@@ -84,6 +84,31 @@
'rule test.utils.TestUtilClass$TestInnerClassTest jarjar.prefix.@0\n',
'rule test.utils.TestUtilClass$TestInnerClassTest$* jarjar.prefix.@0\n'], lines)
+ def test_gen_rules_repeated_testclass_excluded(self):
+ args = gen_jarjar.parse_arguments([
+ "jarjar-rules-generator-testjavalib.jar",
+ "--prefix", "jarjar.prefix",
+ "--output", "test-output-rules.txt",
+ "--apistubs", "framework-connectivity.stubs.module_lib.jar",
+ "--unsupportedapi", ":testdata/test-unsupportedappusage.txt",
+ "--excludes", "testdata/test-jarjar-excludes-testclass.txt",
+ ])
+ gen_jarjar.make_jarjar_rules(args)
+
+ with open(args.output) as out:
+ lines = out.readlines()
+
+ self.maxDiff = None
+ self.assertListEqual([
+ 'rule android.net.IpSecTransform jarjar.prefix.@0\n',
+ 'rule test.unsupportedappusage.OtherUnsupportedUsageClass jarjar.prefix.@0\n',
+ 'rule test.unsupportedappusage.OtherUnsupportedUsageClassTest jarjar.prefix.@0\n',
+ 'rule test.unsupportedappusage.OtherUnsupportedUsageClassTest$* jarjar.prefix.@0\n',
+ 'rule test.utils.TestUtilClass jarjar.prefix.@0\n',
+ 'rule test.utils.TestUtilClass$TestInnerClass jarjar.prefix.@0\n',
+ 'rule test.utils.TestUtilClass$TestInnerClassTest jarjar.prefix.@0\n',
+ 'rule test.utils.TestUtilClass$TestInnerClassTest$* jarjar.prefix.@0\n'], lines)
+
if __name__ == '__main__':
# Need verbosity=2 for the test results parser to find results
diff --git a/tools/testdata/test-jarjar-excludes-testclass.txt b/tools/testdata/test-jarjar-excludes-testclass.txt
new file mode 100644
index 0000000..f7cc2cb
--- /dev/null
+++ b/tools/testdata/test-jarjar-excludes-testclass.txt
@@ -0,0 +1,7 @@
+# Test file for excluded classes
+test\.jarj.rexcluded\.JarjarExcludedCla.s
+test\.jarjarexcluded\.JarjarExcludedClass\$TestInnerCl.ss
+
+# Exclude actual test files
+test\.utils\.TestUtilClassTest
+android\.net\.IpSecTransformTest
\ No newline at end of file