Merge "Separate Struct.java into its own lib." into main
diff --git a/Cronet/tests/common/Android.bp b/Cronet/tests/common/Android.bp
index edeb0b3..703f544 100644
--- a/Cronet/tests/common/Android.bp
+++ b/Cronet/tests/common/Android.bp
@@ -43,6 +43,7 @@
jni_libs: [
"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 bded8fb..ae6b65b 100644
--- a/Cronet/tests/common/AndroidTest.xml
+++ b/Cronet/tests/common/AndroidTest.xml
@@ -35,26 +35,17 @@
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
<!-- b/298380508 -->
<option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsIgnoredInNativeCronetEngineBuilderImpl" />
- <!-- b/316571753 -->
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testBaseFeatureFlagsOverridesEnabled" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAppIdMatches" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAreLoaded" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAtMinVersion" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAboveMinVersion" />
- <!-- b/316567693 -->
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestTest#testSSLCertificateError" />
<!-- b/316559294 -->
<option name="exclude-filter" value="org.chromium.net.NQETest#testQuicDisabled" />
<!-- b/316559294 -->
<option name="exclude-filter" value="org.chromium.net.NQETest#testPrefsWriteRead" />
<!-- b/316554711-->
- <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" />
+ <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/mts/AndroidTest.xml b/Cronet/tests/mts/AndroidTest.xml
index bccbe29..5aed655 100644
--- a/Cronet/tests/mts/AndroidTest.xml
+++ b/Cronet/tests/mts/AndroidTest.xml
@@ -35,16 +35,6 @@
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
<!-- b/298380508 -->
<option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsIgnoredInNativeCronetEngineBuilderImpl" />
- <!-- b/316571753 -->
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testBaseFeatureFlagsOverridesEnabled" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAppIdMatches" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAreLoaded" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAtMinVersion" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAboveMinVersion" />
- <!-- b/316567693 -->
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestTest#testSSLCertificateError" />
<!-- b/316559294 -->
<option name="exclude-filter" value="org.chromium.net.NQETest#testQuicDisabled" />
<!-- b/316559294 -->
@@ -55,6 +45,7 @@
<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/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/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/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 30bdf37..4bae221 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -103,9 +103,9 @@
"dscpPolicy.o",
"netd.o",
"offload.o",
- "offload@btf.o",
+ "offload@mainline.o",
"test.o",
- "test@btf.o",
+ "test@mainline.o",
],
apps: [
"ServiceConnectivityResources",
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 2933a44..f696885 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -83,8 +83,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;
@@ -146,6 +148,8 @@
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;
@@ -172,6 +176,7 @@
// 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;
@@ -201,6 +206,7 @@
assumeTrue(mRunTests);
mTetheredInterfaceRequester = new TetheredInterfaceRequester();
+ sCallbackErrors.clear();
}
private boolean isEthernetTetheringSupported() throws Exception {
@@ -280,6 +286,10 @@
mHandlerThread.quitSafely();
mHandlerThread.join();
}
+
+ if (sCallbackErrors.size() > 0) {
+ fail("Some callbacks had errors: " + sCallbackErrors);
+ }
}
protected static boolean isInterfaceForTetheringAvailable() throws Exception {
@@ -391,7 +401,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 +422,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 +491,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 +546,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 +577,7 @@
@Override
public void onTetheringFailed(int resultCode) {
- fail("Unexpectedly got onTetheringFailed");
+ addCallbackError("Unexpectedly got onTetheringFailed");
}
};
Log.d(TAG, "Starting Ethernet tethering");
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 750bfce..f01e1bb 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -3622,6 +3622,43 @@
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);
+ // FIXME: wifi tethering doesn't have upstream when P2P is enabled.
+ verify(mUpstreamNetworkMonitor, never()).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 674cd98..1958aa8 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -94,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",
],
}
@@ -114,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 5e401aa..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,
@@ -516,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) {
@@ -526,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) {
@@ -548,7 +548,7 @@
// 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", "",
IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
(struct __sk_buff* skb) {
@@ -558,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) {
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/common/flags.aconfig b/common/flags.aconfig
index 19b522c..30931df 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -6,6 +6,7 @@
flag {
name: "set_data_saver_via_cm"
+ is_exported: true
namespace: "android_core_networking"
description: "Set data saver through ConnectivityManager API"
bug: "297836825"
@@ -13,6 +14,7 @@
flag {
name: "support_is_uid_networking_blocked"
+ is_exported: true
namespace: "android_core_networking"
description: "This flag controls whether isUidNetworkingBlocked is supported"
bug: "297836825"
@@ -20,6 +22,7 @@
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"
@@ -27,6 +30,7 @@
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"
@@ -34,6 +38,7 @@
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"
@@ -41,6 +46,7 @@
flag {
name: "request_restricted_wifi"
+ is_exported: true
namespace: "android_core_networking"
description: "Flag for API to support requesting restricted wifi"
bug: "315835605"
@@ -48,6 +54,7 @@
flag {
name: "net_capability_local_network"
+ is_exported: true
namespace: "android_core_networking"
description: "Flag for local network capability API"
bug: "313000440"
@@ -55,6 +62,7 @@
flag {
name: "support_transport_satellite"
+ is_exported: true
namespace: "android_core_networking"
description: "Flag for satellite transport API"
bug: "320514105"
@@ -62,6 +70,7 @@
flag {
name: "nsd_subtypes_support_enabled"
+ is_exported: true
namespace: "android_core_networking"
description: "Flag for API to support nsd subtypes"
bug: "265095929"
@@ -69,6 +78,7 @@
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
index b957d33..b733849 100644
--- a/common/nearby_flags.aconfig
+++ b/common/nearby_flags.aconfig
@@ -3,6 +3,7 @@
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/common/thread_flags.aconfig b/common/thread_flags.aconfig
index 09595a6..43fc147 100644
--- a/common/thread_flags.aconfig
+++ b/common/thread_flags.aconfig
@@ -3,6 +3,7 @@
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/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/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
index 2895b0c..6afb2d5 100644
--- a/framework-t/src/android/net/nsd/AdvertisingRequest.java
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -110,8 +110,9 @@
}
/**
- * Returns the time interval that the resource records may be cached on a DNS resolver or
- * {@code null} if not specified.
+ * 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
*/
@@ -161,7 +162,7 @@
dest.writeParcelable(mServiceInfo, flags);
dest.writeInt(mProtocolType);
dest.writeLong(mAdvertisingConfig);
- dest.writeLong(mTtl == null ? -1 : mTtl.getSeconds());
+ dest.writeLong(mTtl == null ? -1L : mTtl.getSeconds());
}
// @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
@@ -205,7 +206,9 @@
* When registering a service, {@link NsdManager#FAILURE_BAD_PARAMETERS} will be returned
* if {@code ttl} is smaller than 30 seconds.
*
- * Note: only number of seconds of {@code ttl} is used.
+ * 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
*
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index f4cc2ac..dba08a1 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -70,7 +70,8 @@
private int mInterfaceIndex;
- // The timestamp that all resource records associated with this service are considered invalid.
+ // The timestamp that one or more resource records associated with this service are considered
+ // invalid.
@Nullable
private Instant mExpirationTime;
@@ -497,7 +498,9 @@
/**
* Sets the timestamp after when this service is expired.
*
- * Note: only number of seconds of {@code expirationTime} is used.
+ * 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
*/
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 0be30bb..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() {}
@@ -133,4 +168,128 @@
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 915ec52..b1e636d 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -6284,15 +6284,16 @@
@RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
if (!SdkLevel.isAtLeastU()) {
- Log.wtf(TAG, "isUidNetworkingBlocked is not supported on pre-U devices");
+ throw new IllegalStateException(
+ "isUidNetworkingBlocked is not supported on pre-U devices");
}
- final BpfNetMapsReader reader = BpfNetMapsReader.getInstance();
+ 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/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 4de02ac..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;
@@ -41,8 +40,6 @@
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;
@@ -291,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
@@ -318,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);
- }
}
/**
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/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/netbpfload/Android.bp b/netbpfload/Android.bp
index f397b37..c39b46c 100644
--- a/netbpfload/Android.bp
+++ b/netbpfload/Android.bp
@@ -19,19 +19,15 @@
}
install_symlink {
- name: "platform_ethtool_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",
-}
-phony {
- name: "mainline_tethering_platform_components",
- required: [
- "netbpfload",
- "platform_ethtool_symlink",
- ],
+ init_rc: ["netbpfload.rc"],
+ required: ["bpfloader"],
}
cc_binary {
@@ -65,9 +61,7 @@
// module "netbpfload" variant "android_x86_apex30": should support
// min_sdk_version(30) for "com.android.tethering": newer SDK(34).
min_sdk_version: "30",
-
- init_rc: ["netbpfload.rc"],
- required: ["bpfloader"],
+ installable: false,
}
// Versioned netbpfload init rc: init system will process it only on api T/33+ devices
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index ed7d048..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));
@@ -171,8 +171,6 @@
#define APEX_MOUNT_POINT "/apex/com.android.tethering"
const char * const platformBpfLoader = "/system/bin/bpfloader";
-const char * const platformNetBpfLoad = "/system/bin/netbpfload";
-const char * const apexNetBpfLoad = APEX_MOUNT_POINT "/bin/netbpfload";
int logTetheringApexVersion(void) {
char * found_blockdev = NULL;
@@ -232,12 +230,6 @@
ALOGI("NetBpfLoad '%s' starting...", argv[0]);
- // true iff we are running from the module
- const bool is_mainline = !strcmp(argv[0], apexNetBpfLoad);
-
- // true iff we are running from the platform
- const bool is_platform = !strcmp(argv[0], platformNetBpfLoad);
-
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__);
@@ -248,24 +240,11 @@
// 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 platform:%d mainline:%d rc:%d%d",
+ ALOGI("NetBpfLoad api:%d/%d kver:%07x rc:%d%d",
android_get_application_target_sdk_version(), device_api_level,
- android::bpf::kernelVersion(), is_platform, is_mainline,
+ android::bpf::kernelVersion(),
has_platform_bpfloader_rc, has_platform_netbpfload_rc);
- if (!is_platform && !is_mainline) {
- ALOGE("Unable to determine if we're platform or mainline netbpfload.");
- return 1;
- }
-
- if (is_platform) {
- ALOGI("Executing apex netbpfload...");
- const char * args[] = { apexNetBpfLoad, NULL, };
- execve(args[0], (char**)args, envp);
- ALOGE("exec '%s' fail: %d[%s]", apexNetBpfLoad, errno, strerror(errno));
- return 1;
- }
-
if (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
ALOGE("Unable to find platform's bpfloader & netbpfload init scripts.");
return 1;
@@ -278,13 +257,8 @@
logTetheringApexVersion();
- if (is_mainline && has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
- // Tethering apex shipped initrc file causes us to reach here
- // but we're not ready to correctly handle anything before U QPR2
- // in which the 'bpfloader' vs 'netbpfload' split happened
- const char * args[] = { platformBpfLoader, NULL, };
- execve(args[0], (char**)args, envp);
- ALOGE("exec '%s' fail: %d[%s]", platformBpfLoader, errno, strerror(errno));
+ if (!isAtLeastT) {
+ ALOGE("Impossible - not reachable on Android <T.");
return 1;
}
@@ -339,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
@@ -376,9 +352,15 @@
// 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 "
@@ -398,10 +380,15 @@
return 1;
}
- ALOGI("done, transferring control to platform bpfloader.");
+ if (false && isAtLeastV) {
+ ALOGI("done, transferring control to platform bpfloader.");
- const char * args[] = { platformBpfLoader, NULL, };
- execve(args[0], (char**)args, envp);
- ALOGE("FATAL: execve('%s'): %d[%s]", platformBpfLoader, errno, strerror(errno));
- return 1;
+ const char * args[] = { platformBpfLoader, NULL, };
+ execve(args[0], (char**)args, envp);
+ ALOGE("FATAL: execve('%s'): %d[%s]", platformBpfLoader, errno, strerror(errno));
+ 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
index 0ac5de8..d38a503 100644
--- a/netbpfload/netbpfload.mainline.rc
+++ b/netbpfload/netbpfload.mainline.rc
@@ -1,8 +1,17 @@
-service bpfloader /apex/com.android.tethering/bin/netbpfload
+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/BpfHandler.cpp b/netd/BpfHandler.cpp
index a00c363..e6fc825 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -165,8 +165,35 @@
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
+ 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");
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index cfb1a33..aca386f 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -1851,6 +1851,8 @@
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();
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..e61555a 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,15 @@
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;
EnqueueMdnsQueryCallable(
@NonNull MdnsSocketClientBase requestSender,
- @NonNull MdnsPacketWriter packetWriter,
@NonNull String serviceType,
@NonNull Collection<String> subtypes,
boolean expectUnicastResponse,
@@ -93,9 +96,10 @@
boolean sendDiscoveryQueries,
@NonNull Collection<MdnsResponse> servicesToResolve,
@NonNull MdnsUtils.Clock clock,
- @NonNull SharedLog sharedLog) {
+ @NonNull SharedLog sharedLog,
+ @NonNull MdnsServiceTypeClient.Dependencies dependencies,
+ @NonNull Collection<MdnsResponse> existingServices) {
weakRequestSender = new WeakReference<>(requestSender);
- this.packetWriter = packetWriter;
serviceTypeLabels = TextUtils.split(serviceType, "\\.");
this.subtypes = new ArrayList<>(subtypes);
this.expectUnicastResponse = expectUnicastResponse;
@@ -106,6 +110,8 @@
this.servicesToResolve = new ArrayList<>(servicesToResolve);
this.clock = clock;
this.sharedLog = sharedLog;
+ this.dependencies = dependencies;
+ this.existingServices = new ArrayList<>(existingServices);
}
/**
@@ -176,29 +182,52 @@
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 DatagramPacket packet = dependencies.getDatagramPacketFromMdnsPacket(
+ packetCreationBuffer, mdnsPacket, address);
if (expectUnicastResponse) {
// MdnsMultinetworkSocketClient is only available on T+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
@@ -222,16 +251,17 @@
}
}
- 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 c162bcc..98c2d86 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -241,13 +241,10 @@
}
@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 */);
}
};
@@ -318,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
@@ -336,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
@@ -528,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
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 21b7069..7b0c738 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -362,7 +362,7 @@
return new MdnsServiceTypeClient(
serviceType, socketClient,
executorProvider.newServiceTypeClientSchedulerExecutor(), socketKey,
- sharedLog.forSubComponent(tag), looper, serviceCache);
+ sharedLog.forSubComponent(tag), looper, serviceCache, mdnsFeatureFlags);
}
/**
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 56202fd..f4a08ba 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -62,6 +62,11 @@
*/
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;
@@ -83,6 +88,9 @@
// Flag for aggressive query mode
public final boolean mIsAggressiveQueryModeEnabled;
+ // Flag for query with known-answer
+ public final boolean mIsQueryWithKnownAnswerEnabled;
+
@Nullable
private final FlagOverrideProvider mOverrideProvider;
@@ -126,6 +134,14 @@
}
/**
+ * 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}.
*/
public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
@@ -135,6 +151,7 @@
boolean isKnownAnswerSuppressionEnabled,
boolean isUnicastReplyEnabled,
boolean isAggressiveQueryModeEnabled,
+ boolean isQueryWithKnownAnswerEnabled,
@Nullable FlagOverrideProvider overrideProvider) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
@@ -143,6 +160,7 @@
mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
mIsUnicastReplyEnabled = isUnicastReplyEnabled;
mIsAggressiveQueryModeEnabled = isAggressiveQueryModeEnabled;
+ mIsQueryWithKnownAnswerEnabled = isQueryWithKnownAnswerEnabled;
mOverrideProvider = overrideProvider;
}
@@ -162,6 +180,7 @@
private boolean mIsKnownAnswerSuppressionEnabled;
private boolean mIsUnicastReplyEnabled;
private boolean mIsAggressiveQueryModeEnabled;
+ private boolean mIsQueryWithKnownAnswerEnabled;
private FlagOverrideProvider mOverrideProvider;
/**
@@ -175,6 +194,7 @@
mIsKnownAnswerSuppressionEnabled = false;
mIsUnicastReplyEnabled = true;
mIsAggressiveQueryModeEnabled = false;
+ mIsQueryWithKnownAnswerEnabled = false;
mOverrideProvider = null;
}
@@ -261,6 +281,16 @@
}
/**
+ * 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() {
@@ -271,6 +301,7 @@
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 c2363c0..0b2003f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -22,12 +22,10 @@
import android.annotation.Nullable;
import android.annotation.RequiresApi;
import android.net.LinkAddress;
-import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
-import android.util.ArraySet;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.HexDump;
@@ -38,6 +36,7 @@
import java.io.IOException;
import java.net.InetSocketAddress;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -102,12 +101,15 @@
@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);
}
/**
@@ -133,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);
+ }
}
}
@@ -149,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);
+ }
+ });
}
}
}
@@ -234,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);
@@ -281,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
@@ -294,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);
}
}
@@ -330,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()) {
@@ -339,7 +365,6 @@
}
mReplySender.cancelAll();
mSocket.removePacketHandler(this);
- mCbHandler.post(() -> mCb.onDestroyed(mSocket));
}
/**
@@ -442,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/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/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index ac64c3a..073e465 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -925,22 +925,79 @@
}
}
+ @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 {
-
- int serviceId = 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);
+ 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<>();
@@ -972,8 +1029,8 @@
addNsecRecordsForUniqueNames(additionalAnswers,
mGeneralRecords.iterator(), registration.allRecords.iterator());
- return new MdnsAnnouncer.AnnouncementInfo(
- probeSuccessInfo.getServiceId(), new ArrayList<>(answersSet), additionalAnswers);
+ return new MdnsAnnouncer.AnnouncementInfo(serviceId,
+ new ArrayList<>(answersSet), additionalAnswers);
}
/**
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 f60a95e..1ec9e39 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -94,57 +94,6 @@
@NonNull
private final Instant expirationTime;
- /** 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,
- /* expirationTime= */ Instant.MAX);
- }
-
- /** 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,
- /* expirationTime= */ Instant.MAX);
- }
-
/**
* Constructs a {@link MdnsServiceInfo} object with default values.
*
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 8f41b94..bfcd0b4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -35,9 +35,12 @@
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;
@@ -55,7 +58,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;
@@ -84,6 +86,7 @@
notifyRemovedServiceToListeners(previousResponse, "Service record expired");
}
};
+ @NonNull private final MdnsFeatureFlags featureFlags;
private final ArrayMap<MdnsServiceBrowserListener, ListenerInfo> listeners =
new ArrayMap<>();
private final boolean removeServiceAfterTtlExpires =
@@ -142,7 +145,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,
- getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners));
+ getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners),
+ getExistingServices());
executor.submit(queryTask);
break;
}
@@ -221,6 +225,16 @@
public void sendMessage(@NonNull Handler handler, @NonNull Message message) {
handler.sendMessage(message);
}
+
+ /**
+ * Generate a DatagramPacket from given MdnsPacket and InetSocketAddress.
+ */
+ public DatagramPacket getDatagramPacketFromMdnsPacket(@NonNull byte[] packetCreationBuffer,
+ @NonNull MdnsPacket packet, @NonNull InetSocketAddress address) throws IOException {
+ final byte[] queryBuffer =
+ MdnsUtils.createRawDnsPacket(packetCreationBuffer, packet);
+ return new DatagramPacket(queryBuffer, 0, queryBuffer.length, address);
+ }
}
/**
@@ -236,9 +250,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
@@ -251,7 +266,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;
@@ -265,6 +281,7 @@
this.serviceCache = serviceCache;
this.mdnsQueryScheduler = new MdnsQueryScheduler();
this.cacheKey = new MdnsServiceCache.CacheKey(serviceType, socketKey);
+ this.featureFlags = featureFlags;
}
/**
@@ -327,6 +344,11 @@
now.plusMillis(response.getMinRemainingTtl(now.toEpochMilli())));
}
+ private List<MdnsResponse> getExistingServices() {
+ return featureFlags.isQueryWithKnownAnswerEnabled()
+ ? serviceCache.getCachedServices(cacheKey) : Collections.emptyList();
+ }
+
/**
* Registers {@code listener} for receiving discovery event of mDNS service instances, and
* starts
@@ -391,7 +413,8 @@
final QueryTask queryTask = new QueryTask(
mdnsQueryScheduler.scheduleFirstRun(taskConfig, now,
minRemainingTtl, currentSessionId), servicesToResolve,
- getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners));
+ getAllDiscoverySubtypes(), needSendDiscoveryQueries(listeners),
+ getExistingServices());
executor.submit(queryTask);
}
@@ -617,11 +640,6 @@
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++) {
@@ -694,14 +712,16 @@
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 Collection<MdnsResponse> servicesToResolve,
- @NonNull Collection<String> subtypes,
- boolean sendDiscoveryQueries) {
+ @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
@@ -711,7 +731,6 @@
result =
new EnqueueMdnsQueryCallable(
socketClient,
- createMdnsPacketWriter(),
serviceType,
subtypes,
taskArgs.config.expectUnicastResponse,
@@ -721,7 +740,9 @@
sendDiscoveryQueries,
servicesToResolve,
clock,
- sharedLog)
+ sharedLog,
+ dependencies,
+ existingServices)
.call();
} catch (RuntimeException e) {
sharedLog.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s",
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..7b71e43 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -106,6 +106,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 +119,7 @@
unicastReceiverBuffer = null;
}
this.mdnsFeatureFlags = mdnsFeatureFlags;
+ this.interfaceProvider = new MulticastNetworkInterfaceProvider(context, sharedLog);
}
@Override
@@ -138,6 +140,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 +186,7 @@
}
multicastLock.release();
+ interfaceProvider.stopWatchingConnectivityChanges();
shouldStopSocketLoop = true;
waitForSendThreadToStop();
@@ -482,8 +486,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/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index d553210..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;
/**
@@ -226,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/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 458d64f..9c8fd99 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -382,10 +382,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 +460,7 @@
if (!include) {
removeTestData();
}
- mHandler.post(() -> trackAvailableInterfaces());
+ trackAvailableInterfaces();
});
}
@@ -643,25 +642,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/NetworkStatsObservers.java b/service-t/src/com/android/server/net/NetworkStatsObservers.java
index 21cf351..cab29e3 100644
--- a/service-t/src/com/android/server/net/NetworkStatsObservers.java
+++ b/service-t/src/com/android/server/net/NetworkStatsObservers.java
@@ -32,7 +32,6 @@
import android.net.NetworkTemplate;
import android.net.netstats.IUsageCallback;
import android.os.Handler;
-import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
@@ -46,6 +45,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.PerUidCounter;
+import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -78,8 +78,11 @@
// Sequence number of DataUsageRequests
private final AtomicInteger mNextDataUsageRequestId = new AtomicInteger();
- // Lazily instantiated when an observer is registered.
- private volatile Handler mHandler;
+ private final Handler mHandler;
+
+ NetworkStatsObservers(@NonNull Looper looper) {
+ mHandler = new Handler(Objects.requireNonNull(looper), mHandlerCallback);
+ }
/**
* Creates a wrapper that contains the caller context and a normalized request.
@@ -100,7 +103,7 @@
if (LOG) Log.d(TAG, "Registering observer for " + requestInfo);
mDataUsageRequestsPerUid.incrementCountOrThrow(callingUid);
- getHandler().sendMessage(mHandler.obtainMessage(MSG_REGISTER, requestInfo));
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_REGISTER, requestInfo));
return request;
}
@@ -110,7 +113,7 @@
* <p>It will unregister the observer asynchronously, so it is safe to call from any thread.
*/
public void unregister(DataUsageRequest request, int callingUid) {
- getHandler().sendMessage(mHandler.obtainMessage(MSG_UNREGISTER, callingUid, 0 /* ignore */,
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_UNREGISTER, callingUid, 0 /* ignore */,
request));
}
@@ -125,34 +128,10 @@
long currentTime) {
StatsContext statsContext = new StatsContext(xtSnapshot, uidSnapshot, activeIfaces,
activeUidIfaces, currentTime);
- getHandler().sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATS, statsContext));
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATS, statsContext));
}
- private Handler getHandler() {
- if (mHandler == null) {
- synchronized (this) {
- if (mHandler == null) {
- if (LOGV) Log.v(TAG, "Creating handler");
- mHandler = new Handler(getHandlerLooperLocked(), mHandlerCallback);
- }
- }
- }
- return mHandler;
- }
-
- @VisibleForTesting
- protected Looper getHandlerLooperLocked() {
- // TODO: Currently, callbacks are dispatched on this thread if the caller register
- // callback without supplying a Handler. To ensure that the service handler thread
- // is not blocked by client code, the observers must create their own thread. Once
- // all callbacks are dispatched outside of the handler thread, the service handler
- // thread can be used here.
- HandlerThread handlerThread = new HandlerThread(TAG);
- handlerThread.start();
- return handlerThread.getLooper();
- }
-
- private Handler.Callback mHandlerCallback = new Handler.Callback() {
+ private final Handler.Callback mHandlerCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 9684d18..64b17eb 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -593,7 +593,7 @@
INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)),
alarmManager, wakeLock, getDefaultClock(),
new DefaultNetworkStatsSettings(), new NetworkStatsFactory(context),
- new NetworkStatsObservers(), new Dependencies());
+ new Dependencies());
return service;
}
@@ -603,8 +603,7 @@
@VisibleForTesting
NetworkStatsService(Context context, INetd netd, AlarmManager alarmManager,
PowerManager.WakeLock wakeLock, Clock clock, NetworkStatsSettings settings,
- NetworkStatsFactory factory, NetworkStatsObservers statsObservers,
- @NonNull Dependencies deps) {
+ NetworkStatsFactory factory, @NonNull Dependencies deps) {
mContext = Objects.requireNonNull(context, "missing Context");
mNetd = Objects.requireNonNull(netd, "missing Netd");
mAlarmManager = Objects.requireNonNull(alarmManager, "missing AlarmManager");
@@ -612,7 +611,6 @@
mSettings = Objects.requireNonNull(settings, "missing NetworkStatsSettings");
mWakeLock = Objects.requireNonNull(wakeLock, "missing WakeLock");
mStatsFactory = Objects.requireNonNull(factory, "missing factory");
- mStatsObservers = Objects.requireNonNull(statsObservers, "missing NetworkStatsObservers");
mDeps = Objects.requireNonNull(deps, "missing Dependencies");
mStatsDir = mDeps.getOrCreateStatsDir();
if (!mStatsDir.exists()) {
@@ -622,6 +620,7 @@
final HandlerThread handlerThread = mDeps.makeHandlerThread();
handlerThread.start();
mHandler = new NetworkStatsHandler(handlerThread.getLooper());
+ mStatsObservers = new NetworkStatsObservers(handlerThread.getLooper());
mNetworkStatsSubscriptionsMonitor = deps.makeSubscriptionsMonitor(mContext,
(command) -> mHandler.post(command) , this);
mContentResolver = mContext.getContentResolver();
diff --git a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
index 8598ac4..ca97d07 100644
--- a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -19,12 +19,13 @@
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.HashMap;
import java.util.Objects;
+import java.util.function.Supplier;
/**
* A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
@@ -39,10 +40,12 @@
*
* @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) {
+ TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs, int maxSize) {
mClock = clock;
mExpiryDurationMs = expiryDurationMs;
+ mMap = new LruCache<>(maxSize);
}
private static class TrafficStatsCacheKey {
@@ -81,7 +84,7 @@
}
@GuardedBy("mMap")
- private final HashMap<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap = new HashMap<>();
+ private final LruCache<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap;
/**
* Retrieves a {@link NetworkStats.Entry} from the cache, associated with the given key.
@@ -105,6 +108,36 @@
}
/**
+ * 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.
@@ -124,7 +157,7 @@
*/
void clear() {
synchronized (mMap) {
- mMap.clear();
+ mMap.evictAll();
}
}
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index f7e47f5..4783f2b 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -31,4 +31,26 @@
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 d9af5a3..158b0c8 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -48,6 +48,9 @@
<!-- 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/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index a7fddd0..fc6d8c4 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -49,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;
@@ -535,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) {
@@ -635,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 {
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 30b14b2..123ad8f 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -97,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;
@@ -115,6 +117,9 @@
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;
@@ -214,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;
@@ -844,11 +848,6 @@
private static final int EVENT_UID_FROZEN_STATE_CHANGED = 61;
/**
- * Event to inform the ConnectivityService handler when a uid has lost carrier privileges.
- */
- private static final int EVENT_UID_CARRIER_PRIVILEGES_LOST = 62;
-
- /**
* Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification
* should be shown.
*/
@@ -1001,6 +1000,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.
*
@@ -1288,14 +1290,6 @@
}
private final LegacyTypeTracker mLegacyTypeTracker = new LegacyTypeTracker(this);
- @VisibleForTesting
- void onCarrierPrivilegesLost(Integer uid, Integer subId) {
- if (mRequestRestrictedWifiEnabled) {
- mHandler.sendMessage(mHandler.obtainMessage(
- EVENT_UID_CARRIER_PRIVILEGES_LOST, uid, subId));
- }
- }
-
final LocalPriorityDump mPriorityDumper = new LocalPriorityDump();
/**
* Helper class which parses out priority arguments and dumps sections according to their
@@ -1517,10 +1511,11 @@
@NonNull final Context context,
@NonNull final TelephonyManager tm,
boolean requestRestrictedWifiEnabled,
- @NonNull BiConsumer<Integer, Integer> listener) {
+ @NonNull BiConsumer<Integer, Integer> listener,
+ @NonNull final Handler connectivityServiceHandler) {
if (isAtLeastT()) {
- return new CarrierPrivilegeAuthenticator(
- context, tm, requestRestrictedWifiEnabled, listener);
+ return new CarrierPrivilegeAuthenticator(context, tm, requestRestrictedWifiEnabled,
+ listener, connectivityServiceHandler);
} else {
return null;
}
@@ -1805,7 +1800,7 @@
&& mDeps.isFeatureEnabled(context, REQUEST_RESTRICTED_WIFI);
mCarrierPrivilegeAuthenticator = mDeps.makeCarrierPrivilegeAuthenticator(
mContext, mTelephonyManager, mRequestRestrictedWifiEnabled,
- this::onCarrierPrivilegesLost);
+ this::handleUidCarrierPrivilegesLost, mHandler);
if (mDeps.isAtLeastU()
&& mDeps
@@ -1978,6 +1973,8 @@
activityManager.registerUidFrozenStateChangedCallback(
(Runnable r) -> r.run(), frozenStateChangedCallback);
}
+ mIngressToVpnAddressFiltering = mDeps.isAtLeastT()
+ && mDeps.isFeatureNotChickenedOut(mContext, INGRESS_TO_VPN_ADDRESS_FILTERING);
}
/**
@@ -2607,7 +2604,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();
@@ -2765,6 +2762,7 @@
private boolean canSeeAllowedUids(final int pid, final int uid, final int netOwnerUid) {
return Process.SYSTEM_UID == uid
+ || netOwnerUid == uid
|| hasAnyPermissionOf(mContext, pid, uid,
android.Manifest.permission.NETWORK_FACTORY);
}
@@ -2792,7 +2790,6 @@
}
if (!canSeeAllowedUids(callerPid, callerUid, newNc.getOwnerUid())) {
newNc.setAllowedUids(new ArraySet<>());
- newNc.setSubscriptionIds(Collections.emptySet());
}
redactUnderlyingNetworksForCapabilities(newNc, callerPid, callerUid);
@@ -3020,6 +3017,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();
@@ -3805,6 +3819,10 @@
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);
@@ -5326,6 +5344,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) {
@@ -6003,7 +6022,15 @@
if (nm == null) return;
if (request == CaptivePortal.APP_REQUEST_REEVALUATION_REQUIRED) {
- enforceNetworkStackPermission(mContext);
+ // 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());
}
}
@@ -6484,9 +6511,6 @@
UidFrozenStateChangedArgs args = (UidFrozenStateChangedArgs) msg.obj;
handleFrozenUids(args.mUids, args.mFrozenStates);
break;
- case EVENT_UID_CARRIER_PRIVILEGES_LOST:
- handleUidCarrierPrivilegesLost(msg.arg1, msg.arg2);
- break;
}
}
}
@@ -7566,15 +7590,6 @@
"Insufficient permissions to request a specific signal strength");
}
mAppOpsManager.checkPackage(callerUid, callerPackageName);
-
- if (nc.getSubscriptionIds().isEmpty()) {
- return;
- }
- if (mRequestRestrictedWifiEnabled
- && canRequestRestrictedNetworkDueToCarrierPrivileges(nc, callerUid)) {
- return;
- }
- enforceNetworkFactoryPermission();
}
private int[] getSignalStrengthThresholds(@NonNull final NetworkAgentInfo nai) {
@@ -7708,10 +7723,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);
@@ -7798,7 +7815,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;
}
@@ -7950,8 +7967,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);
@@ -8011,7 +8028,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.
@@ -8044,7 +8061,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);
@@ -8675,6 +8692,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()) {
@@ -8966,6 +8985,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(
@@ -9149,6 +9249,9 @@
}
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
@@ -9156,7 +9259,7 @@
// 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.isListen())
+ if (nr.isRequest()
&& !nr.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
&& nr.getRequestorUid() == uid
&& getSubscriptionIdFromNetworkCaps(nr.networkCapabilities) == subId
@@ -12017,7 +12120,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(
diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
index 04d0fc1..f5fa4fb 100644
--- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
+++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -91,42 +91,62 @@
@NonNull final TelephonyManager t,
@NonNull final TelephonyManagerShim telephonyManagerShim,
final boolean requestRestrictedWifiEnabled,
- @NonNull BiConsumer<Integer, Integer> listener) {
+ @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, final boolean requestRestrictedWifiEnabled,
- @NonNull BiConsumer<Integer, Integer> listener) {
+ @NonNull BiConsumer<Integer, Integer> listener,
+ @NonNull final Handler connectivityServiceHandler) {
this(c, new Dependencies(), t, TelephonyManagerShimImpl.newInstance(t),
- requestRestrictedWifiEnabled, listener);
+ requestRestrictedWifiEnabled, listener, connectivityServiceHandler);
}
public static class Dependencies {
@@ -146,6 +166,10 @@
}
private void simConfigChanged() {
+ // If mRequestRestrictedWifiEnabled is false, constructor calls simConfigChanged
+ if (mRequestRestrictedWifiEnabled) {
+ ensureRunningOnHandlerThread();
+ }
synchronized (mLock) {
unregisterCarrierPrivilegesListeners();
mModemCount = mTelephonyManager.getActiveModemCount();
@@ -188,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
@@ -198,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
@@ -439,6 +465,13 @@
}
}
+ private void ensureRunningOnHandlerThread() {
+ if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+ throw new IllegalStateException(
+ "Not running on handler thread: " + Thread.currentThread().getName());
+ }
+ }
+
public void dump(IndentingPrintWriter pw) {
pw.println("CarrierPrivilegeAuthenticator:");
pw.println("mRequestRestrictedWifiEnabled = " + mRequestRestrictedWifiEnabled);
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index daaf91d..eea16bf 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -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 bf09160..a55c683 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -38,6 +38,10 @@
public static final String REQUEST_RESTRICTED_WIFI =
"request_restricted_wifi";
+
+ public static final String INGRESS_TO_VPN_ADDRESS_FILTERING =
+ "ingress_to_vpn_address_filtering";
+
private boolean mNoRematchAllRequestsOnRegister;
/**
diff --git a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
index 48af9fa..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;
@@ -279,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/staticlibs/device/com/android/net/module/util/structs/PrefixInformationOption.java b/staticlibs/device/com/android/net/module/util/structs/PrefixInformationOption.java
index 49d7654..0fc85e4 100644
--- a/staticlibs/device/com/android/net/module/util/structs/PrefixInformationOption.java
+++ b/staticlibs/device/com/android/net/module/util/structs/PrefixInformationOption.java
@@ -21,6 +21,7 @@
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.Field;
@@ -71,7 +72,8 @@
@Field(order = 7, type = Type.ByteArray, arraysize = 16)
public final byte[] prefix;
- PrefixInformationOption(final byte type, final byte length, final byte prefixLen,
+ @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;
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/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/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..f81a03d 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
@@ -24,6 +24,7 @@
import static android.content.pm.PackageManager.FEATURE_WIFI;
import static android.net.ConnectivityManager.TYPE_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.os.Process.INVALID_UID;
import static android.system.OsConstants.AF_INET;
import static android.system.OsConstants.AF_INET6;
@@ -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;
@@ -249,6 +252,7 @@
@Before
public void setUp() throws Exception {
+ assumeTrue(supportedHardware());
mNetwork = null;
mTestContext = getInstrumentation().getContext();
mTargetContext = getInstrumentation().getTargetContext();
@@ -879,7 +883,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();
@@ -938,7 +941,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 +1019,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 +1028,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 +1142,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 +1229,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 +1261,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 +1284,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 +1304,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 +1321,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 +1348,6 @@
@Test
public void testGetConnectionOwnerUidSecurity() throws Exception {
- assumeTrue(supportedHardware());
-
DatagramSocket s;
InetAddress address = InetAddress.getByName("localhost");
s = new DatagramSocket();
@@ -1375,7 +1368,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 +1407,6 @@
@Test
public void testSetProxyDisallowedApps() throws Exception {
- assumeTrue(supportedHardware());
ProxyInfo initialProxy = mCM.getDefaultProxy();
String disallowedApps = mPackageName;
@@ -1441,7 +1432,6 @@
@Test
public void testNoProxy() throws Exception {
- assumeTrue(supportedHardware());
ProxyInfo initialProxy = mCM.getDefaultProxy();
BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
proxyBroadcastReceiver.register();
@@ -1476,7 +1466,6 @@
@Test
public void testBindToNetworkWithProxy() throws Exception {
- assumeTrue(supportedHardware());
String allowedApps = mPackageName;
Network initialNetwork = mCM.getActiveNetwork();
ProxyInfo initialProxy = mCM.getDefaultProxy();
@@ -1501,9 +1490,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 +1519,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 +1545,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 +1584,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 +1608,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 +1645,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 +1752,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 +1807,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 +1875,6 @@
@Test
public void testSetVpnDefaultForUids() throws Exception {
- assumeTrue(supportedHardware());
assumeTrue(SdkLevel.isAtLeastU());
final Network defaultNetwork = mCM.getActiveNetwork();
@@ -1953,6 +1920,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 +2038,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 +2047,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 +2081,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/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/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/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 dbececf..6dd4857 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -1888,6 +1888,64 @@
}
}
+ @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
@@ -2108,6 +2166,106 @@
}
@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
@@ -2159,15 +2317,10 @@
}
private fun hasServiceTypeClientsForNetwork(clients: List<String>, network: Network): Boolean {
- for (client in clients) {
- val netid = client.substring(
- client.indexOf("network=") + "network=".length,
- client.indexOf("interfaceIndex=") - 1)
- if (netid == network.toString()) {
- return true
- }
+ return clients.any { client -> client.substring(
+ client.indexOf("network=") + "network=".length,
+ client.indexOf("interfaceIndex=") - 1) == network.getNetId().toString()
}
- return false
}
/**
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index 3928961..1023173 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -47,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"],
}
@@ -80,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"],
@@ -93,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/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 361d68c..d2e46af 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -66,6 +66,8 @@
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
@@ -87,8 +89,6 @@
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import org.mockito.Spy
-import java.util.function.Consumer
-import java.util.function.BiConsumer
const val SERVICE_BIND_TIMEOUT_MS = 5_000L
const val TEST_TIMEOUT_MS = 10_000L
@@ -225,6 +225,7 @@
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,
@@ -242,18 +243,19 @@
super.makeHandlerThread(tag).also { handlerThreads.add(it) }
override fun makeCarrierPrivilegeAuthenticator(
- context: Context,
- tm: TelephonyManager,
- requestRestrictedWifiEnabled: Boolean,
- listener: BiConsumer<Int, Int>
+ 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)
+ object : CarrierPrivilegeAuthenticator.Dependencies() {
+ override fun makeHandlerThread(): HandlerThread =
+ super.makeHandlerThread().also { handlerThreads.add(it) }
+ },
+ tm, TelephonyManagerShimImpl.newInstance(tm),
+ requestRestrictedWifiEnabled, listener, handler)
}
override fun makeSatelliteAccessController(
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/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/unit/Android.bp b/tests/unit/Android.bp
index a5d2f4a..2f88c41 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -72,17 +72,6 @@
],
}
-// 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 {
name: "FrameworksNetTestsDefaults",
min_sdk_version: "30",
@@ -109,7 +98,6 @@
"platform-test-annotations",
"service-connectivity-pre-jarjar",
"service-connectivity-tiramisu-pre-jarjar",
- "services.core-vpn",
"testables",
"cts-net-utils",
],
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/nsd/AdvertisingRequestTest.kt b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
index 332f2a3..c491f37 100644
--- a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
+++ b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
@@ -54,17 +54,17 @@
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()
+ @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)
-}
+ assertNull(request.ttl)
+ }
@Test
fun testBuilder_setPropertiesSuccess() {
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 76a649e..27c4561 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -240,7 +240,7 @@
AdvertisingRequest capturedRequest = getAdvertisingRequest(
req -> verify(mServiceConn).registerService(anyInt(), req.capture()));
- assertEquals(request, capturedRequest);
+ assertEquals(request.getTtl(), capturedRequest.getTtl());
}
private void doTestRegisterService() throws Exception {
diff --git a/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
index 53baee1..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,13 +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
@@ -31,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>()
@@ -80,7 +90,7 @@
@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)
@@ -179,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/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 8f768b2..1f8a743 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -172,6 +172,7 @@
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.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 +368,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,7 +388,6 @@
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;
@@ -423,7 +422,6 @@
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;
@@ -463,7 +461,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;
@@ -630,7 +627,6 @@
@Mock TelephonyManager mTelephonyManager;
@Mock EthernetManager mEthernetManager;
@Mock NetworkPolicyManager mNetworkPolicyManager;
- @Mock VpnProfileStore mVpnProfileStore;
@Mock SystemConfigManager mSystemConfigManager;
@Mock DevicePolicyManager mDevicePolicyManager;
@Mock Resources mResources;
@@ -1666,23 +1662,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();
}
}
@@ -2054,12 +2038,16 @@
};
}
+ private BiConsumer<Integer, Integer> mCarrierPrivilegesLostListener;
+
@Override
public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
@NonNull final Context context,
@NonNull final TelephonyManager tm,
final boolean requestRestrictedWifiEnabled,
- BiConsumer<Integer, Integer> listener) {
+ BiConsumer<Integer, Integer> listener,
+ @NonNull final Handler handler) {
+ mCarrierPrivilegesLostListener = listener;
return mDeps.isAtLeastT() ? mCarrierPrivilegeAuthenticator : null;
}
@@ -2179,6 +2167,8 @@
return true;
case ALLOW_SATALLITE_NETWORK_FALLBACK:
return true;
+ case INGRESS_TO_VPN_ADDRESS_FILTERING:
+ return true;
default:
return super.isFeatureNotChickenedOut(context, name);
}
@@ -10210,24 +10200,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);
@@ -10239,7 +10211,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);
@@ -10277,8 +10249,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);
@@ -10302,7 +10274,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);
@@ -10348,14 +10320,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);
@@ -10364,7 +10337,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());
@@ -10385,7 +10358,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);
@@ -10411,7 +10384,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);
@@ -10452,14 +10425,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)
@@ -17338,21 +17309,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);
@@ -17378,8 +17335,7 @@
}
@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();
@@ -17395,21 +17351,6 @@
}
@Test
- public void testNetworkRequestWithSubIdsWithoutNetworkFactoryPermission() throws Exception {
- mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED);
- final PendingIntent pendingIntent = PendingIntent.getBroadcast(
- mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE);
-
- 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()));
- }
-
- @Test
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
public void testCarrierConfigAppSendNetworkRequestForRestrictedWifi() throws Exception {
mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED);
@@ -17476,7 +17417,10 @@
.isCarrierServiceUidForNetworkCapabilities(eq(Process.myUid()), any());
doReturn(TEST_SUBSCRIPTION_ID).when(mCarrierPrivilegeAuthenticator)
.getSubIdFromNetworkCapabilities(any());
- mService.onCarrierPrivilegesLost(lostPrivilegeUid, lostPrivilegeSubId);
+
+ visibleOnHandlerThread(mCsHandlerThread.getThreadHandler(), () -> {
+ mDeps.mCarrierPrivilegesLostListener.accept(lostPrivilegeUid, lostPrivilegeSubId);
+ });
waitForIdle();
if (expectCapChanged) {
@@ -17490,11 +17434,12 @@
}
mWiFiAgent.disconnect();
- waitForIdle();
if (expectUnavailable) {
+ testFactory.expectRequestRemove();
testFactory.assertRequestCountEquals(0);
} else {
+ testFactory.expectRequestAdd();
testFactory.assertRequestCountEquals(1);
}
@@ -17544,6 +17489,47 @@
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
public void testAllowedUids() throws Exception {
final int preferenceOrder =
diff --git a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
index 7bd2b56..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,6 +46,7 @@
import android.net.NetworkCapabilities;
import android.net.TelephonyNetworkSpecifier;
import android.os.Build;
+import android.os.Handler;
import android.os.HandlerThread;
import android.telephony.TelephonyManager;
@@ -56,6 +58,7 @@
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;
@@ -85,6 +88,7 @@
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;
@@ -97,13 +101,16 @@
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) {
+ @NonNull final TelephonyManager t,
+ @NonNull final Handler handler) {
super(c, deps, t, mTelephonyManagerShim, true /* requestRestrictedWifiEnabled */,
- mListener);
+ mListener, handler);
}
@Override
protected int getSubId(int slotIndex) {
@@ -112,8 +119,11 @@
}
@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 */
@@ -141,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 ->
@@ -178,7 +194,9 @@
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)
@@ -201,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));
@@ -216,7 +234,9 @@
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(TEST_SUBSCRIPTION_ID);
@@ -235,12 +255,17 @@
public void testCarrierPrivilegesLostDueToCarrierServiceUpdate() throws Exception {
final CarrierPrivilegesListenerShim l = getCarrierPrivilegesListeners().get(0);
- l.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
- l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 1);
+ visibleOnHandlerThread(mCsHandler, () -> {
+ l.onCarrierServiceChanged(null, mCarrierConfigPkgUid);
+ l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 1);
+ });
if (mUseCallbacks) {
verify(mListener).accept(eq(mCarrierConfigPkgUid), eq(TEST_SUBSCRIPTION_ID));
}
- l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 2);
+
+ visibleOnHandlerThread(mCsHandler, () -> {
+ l.onCarrierServiceChanged(null, mCarrierConfigPkgUid + 2);
+ });
if (mUseCallbacks) {
verify(mListener).accept(eq(mCarrierConfigPkgUid + 1), eq(TEST_SUBSCRIPTION_ID));
}
@@ -260,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));
@@ -272,7 +299,9 @@
@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);
@@ -297,7 +326,9 @@
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
public void testNetworkCapabilitiesContainOneSubId() 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_WIFI);
@@ -311,7 +342,9 @@
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
public void testNetworkCapabilitiesContainTwoSubIds() 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_WIFI);
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/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index b8ebf0f..df48f6c 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -286,7 +286,6 @@
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))
}
@@ -364,10 +363,10 @@
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)
}
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 28608bb..629ac67 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -18,7 +18,6 @@
import android.net.InetAddresses.parseNumericAddress
import android.net.LinkAddress
-import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.os.Build
import android.os.HandlerThread
@@ -55,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
@@ -65,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"
@@ -78,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 {
@@ -179,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
@@ -422,8 +517,8 @@
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)
@@ -432,8 +527,15 @@
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/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index 8d1dff6..271cc65 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -24,6 +24,7 @@
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_PTR
@@ -38,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
@@ -50,6 +52,10 @@
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
@@ -111,6 +117,14 @@
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 {
@@ -146,7 +160,7 @@
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
assertEquals(0, repository.servicesCount)
assertEquals(-1,
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */))
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, Duration.ofSeconds(50)))
assertEquals(1, repository.servicesCount)
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -169,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])
@@ -1675,6 +1689,127 @@
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 /* ttlMillis */,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ 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)))
+ }
}
private fun MdnsRecordRepository.initWithService(
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 8740e80..4ce8ba6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
@@ -54,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"));
@@ -73,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"));
@@ -93,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());
@@ -113,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());
@@ -131,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"));
@@ -150,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);
}
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 09236b1..2eb9440 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -38,6 +38,7 @@
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;
@@ -117,8 +118,6 @@
@Mock
private MdnsServiceBrowserListener mockListenerTwo;
@Mock
- private MdnsPacketWriter mockPacketWriter;
- @Mock
private MdnsMultinetworkSocketClient mockSocketClient;
@Mock
private Network mockNetwork;
@@ -145,6 +144,7 @@
private long latestDelayMs = 0;
private Message delayMessage = null;
private Handler realHandler = null;
+ private MdnsFeatureFlags featureFlags = MdnsFeatureFlags.newBuilder().build();
@Before
@SuppressWarnings("DoNotMock")
@@ -162,7 +162,8 @@
expectedIPv6Packets[i] = new DatagramPacket(buf, 0 /* offset */, 5 /* length */,
MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
}
- when(mockPacketWriter.getPacket(IPV4_ADDRESS))
+ when(mockDeps.getDatagramPacketFromMdnsPacket(
+ any(), any(MdnsPacket.class), eq(IPV4_ADDRESS)))
.thenReturn(expectedIPv4Packets[0])
.thenReturn(expectedIPv4Packets[1])
.thenReturn(expectedIPv4Packets[2])
@@ -188,7 +189,8 @@
.thenReturn(expectedIPv4Packets[22])
.thenReturn(expectedIPv4Packets[23]);
- when(mockPacketWriter.getPacket(IPV6_ADDRESS))
+ when(mockDeps.getDatagramPacketFromMdnsPacket(
+ any(), any(MdnsPacket.class), eq(IPV6_ADDRESS)))
.thenReturn(expectedIPv6Packets[0])
.thenReturn(expectedIPv6Packets[1])
.thenReturn(expectedIPv6Packets[2])
@@ -242,22 +244,13 @@
return true;
}).when(mockDeps).sendMessage(any(Handler.class), any(Message.class));
- client = makeMdnsServiceTypeClient(mockPacketWriter);
+ client = makeMdnsServiceTypeClient();
}
- private MdnsServiceTypeClient makeMdnsServiceTypeClient(
- @Nullable MdnsPacketWriter packetWriter) {
+ private MdnsServiceTypeClient makeMdnsServiceTypeClient() {
return new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
- serviceCache) {
- @Override
- MdnsPacketWriter createMdnsPacketWriter() {
- if (packetWriter == null) {
- return super.createMdnsPacketWriter();
- }
- return packetWriter;
- }
- };
+ serviceCache, featureFlags);
}
@After
@@ -697,20 +690,20 @@
@Test
public void testCombinedSubtypesQueriedWithMultipleListeners() throws Exception {
- client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
final MdnsSearchOptions searchOptions1 = MdnsSearchOptions.newBuilder()
.addSubtype("subtype1").build();
final MdnsSearchOptions searchOptions2 = MdnsSearchOptions.newBuilder()
.addSubtype("subtype2").build();
+ doCallRealMethod().when(mockDeps).getDatagramPacketFromMdnsPacket(
+ any(), any(MdnsPacket.class), any(InetSocketAddress.class));
startSendAndReceive(mockListenerOne, searchOptions1);
- currentThreadExecutor.getAndClearSubmittedRunnable().run();
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
InOrder inOrder = inOrder(mockListenerOne, mockSocketClient, mockDeps);
// Verify the query asks for subtype1
final ArgumentCaptor<DatagramPacket> subtype1QueryCaptor =
ArgumentCaptor.forClass(DatagramPacket.class);
- currentThreadExecutor.getAndClearLastScheduledRunnable().run();
// Send twice for IPv4 and IPv6
inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
subtype1QueryCaptor.capture(),
@@ -1022,7 +1015,6 @@
public void processResponse_searchOptionsEnableServiceRemoval_shouldRemove()
throws Exception {
final String serviceInstanceName = "service-instance-1";
- client = makeMdnsServiceTypeClient(mockPacketWriter);
MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
.setRemoveExpiredService(true)
.setNumOfQueriesBeforeBackoff(Integer.MAX_VALUE)
@@ -1060,7 +1052,6 @@
public void processResponse_searchOptionsNotEnableServiceRemoval_shouldNotRemove()
throws Exception {
final String serviceInstanceName = "service-instance-1";
- client = makeMdnsServiceTypeClient(mockPacketWriter);
startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
@@ -1086,7 +1077,6 @@
throws Exception {
//MdnsConfigsFlagsImpl.removeServiceAfterTtlExpires.override(true);
final String serviceInstanceName = "service-instance-1";
- client = makeMdnsServiceTypeClient(mockPacketWriter);
startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
@@ -1201,8 +1191,6 @@
@Test
public void testProcessResponse_Resolve() throws Exception {
- client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
final String instanceName = "service-instance";
final String[] hostname = new String[] { "testhost "};
final String ipV4Address = "192.0.2.0";
@@ -1213,6 +1201,9 @@
final MdnsSearchOptions resolveOptions2 = MdnsSearchOptions.newBuilder()
.setResolveInstanceName(instanceName).build();
+ doCallRealMethod().when(mockDeps).getDatagramPacketFromMdnsPacket(
+ any(), any(MdnsPacket.class), any(InetSocketAddress.class));
+
startSendAndReceive(mockListenerOne, resolveOptions1);
startSendAndReceive(mockListenerTwo, resolveOptions2);
// No need to verify order for both listeners; and order is not guaranteed between them
@@ -1317,8 +1308,6 @@
@Test
public void testRenewTxtSrvInResolve() throws Exception {
- client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
final String instanceName = "service-instance";
final String[] hostname = new String[] { "testhost "};
final String ipV4Address = "192.0.2.0";
@@ -1327,6 +1316,9 @@
final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
.setResolveInstanceName(instanceName).build();
+ doCallRealMethod().when(mockDeps).getDatagramPacketFromMdnsPacket(
+ any(), any(MdnsPacket.class), any(InetSocketAddress.class));
+
startSendAndReceive(mockListenerOne, resolveOptions);
InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
@@ -1431,8 +1423,6 @@
@Test
public void testProcessResponse_ResolveExcludesOtherServices() {
- client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
final String requestedInstance = "instance1";
final String otherInstance = "instance2";
final String ipV4Address = "192.0.2.0";
@@ -1499,8 +1489,6 @@
@Test
public void testProcessResponse_SubtypeDiscoveryLimitedToSubtype() {
- client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
final String matchingInstance = "instance1";
final String subtype = "_subtype";
final String otherInstance = "instance2";
@@ -1587,8 +1575,6 @@
@Test
public void testProcessResponse_SubtypeChange() {
- client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
final String matchingInstance = "instance1";
final String subtype = "_subtype";
final String ipV4Address = "192.0.2.0";
@@ -1670,8 +1656,6 @@
@Test
public void testNotifySocketDestroyed() throws Exception {
- client = makeMdnsServiceTypeClient(/* packetWriter= */ null);
-
final String requestedInstance = "instance1";
final String otherInstance = "instance2";
final String ipV4Address = "192.0.2.0";
@@ -1946,6 +1930,138 @@
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).getDatagramPacketFromMdnsPacket(
+ any(), any(MdnsPacket.class), any(InetSocketAddress.class));
+
+ startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
+
+ final ArgumentCaptor<DatagramPacket> queryCaptor =
+ ArgumentCaptor.forClass(DatagramPacket.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()));
+ 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<DatagramPacket> knownAnswersQueryCaptor =
+ ArgumentCaptor.forClass(DatagramPacket.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()));
+ 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).getDatagramPacketFromMdnsPacket(
+ any(), any(MdnsPacket.class), any(InetSocketAddress.class));
+
+ final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
+ .addSubtype("subtype").build();
+ startSendAndReceive(mockListenerOne, options);
+ InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
+
+ final ArgumentCaptor<DatagramPacket> queryCaptor =
+ ArgumentCaptor.forClass(DatagramPacket.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()));
+ 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<DatagramPacket> knownAnswersQueryCaptor =
+ ArgumentCaptor.forClass(DatagramPacket.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()));
+ 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));
}
@@ -2006,6 +2122,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..7ced1cb 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,17 @@
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.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,6 +51,7 @@
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -71,6 +75,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 +89,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);
@@ -320,19 +328,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());
}
}
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..b1a7233 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
@@ -102,4 +111,67 @@
arrayOf("a", "_other", "_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/CSCaptivePortalAppTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
index be2b29c..0bad60d 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSCaptivePortalAppTest.kt
@@ -20,6 +20,7 @@
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
@@ -33,23 +34,23 @@
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.NetworkStack
-import android.net.CaptivePortal
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.assertThrows
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
-import kotlin.test.assertEquals
// This allows keeping all the networks connected without having to file individual requests
// for them.
@@ -95,16 +96,22 @@
captivePortalCallback.expectAvailableCallbacksUnvalidated(wifiAgent)
val signInIntent = startCaptivePortalApp(wifiAgent)
// Remove the granted permissions
- context.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
- PERMISSION_DENIED)
+ context.setPermission(
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ PERMISSION_DENIED
+ )
context.setPermission(NETWORK_STACK, PERMISSION_DENIED)
val captivePortal: CaptivePortal? = signInIntent.getParcelableExtra(EXTRA_CAPTIVE_PORTAL)
- assertThrows(SecurityException::class.java, { captivePortal?.reevaluateNetwork() })
+ 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))
+ return Agent(
+ score = keepScore(),
+ lp = lp(WIFI_IFACE),
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET)
+ )
}
private fun startCaptivePortalApp(networkAgent: CSAgentWrapper): Intent {
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..e8664c1
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
@@ -0,0 +1,289 @@
+/*
+ * 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.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)
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index c1730a4..83fff87 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -38,6 +38,7 @@
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
@@ -47,12 +48,15 @@
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 org.mockito.Mockito.verifyNoMoreInteractions
private const val TIMEOUT_MS = 200L
private const val MEDIUM_TIMEOUT_MS = 1_000L
@@ -88,10 +92,10 @@
class CSLocalAgentTests : CSTest() {
val multicastRoutingConfigMinScope =
MulticastRoutingConfig.Builder(MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE, 4)
- .build();
+ .build()
val multicastRoutingConfigSelected =
MulticastRoutingConfig.Builder(MulticastRoutingConfig.FORWARD_SELECTED)
- .build();
+ .build()
val upstreamSelectorAny = NetworkRequest.Builder()
.addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
.build()
@@ -205,6 +209,9 @@
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
}
@@ -219,9 +226,12 @@
nc = nc(TRANSPORT_CELLULAR, NET_CAPABILITY_INTERNET))
}
- private fun sendLocalNetworkConfig(localAgent: CSAgentWrapper,
- upstreamSelector: NetworkRequest?, upstreamConfig: MulticastRoutingConfig,
- downstreamConfig: MulticastRoutingConfig) {
+ private fun sendLocalNetworkConfig(
+ localAgent: CSAgentWrapper,
+ upstreamSelector: NetworkRequest?,
+ upstreamConfig: MulticastRoutingConfig,
+ downstreamConfig: MulticastRoutingConfig
+ ) {
val newLnc = LocalNetworkConfig.Builder()
.setUpstreamSelector(upstreamSelector)
.setUpstreamMulticastRoutingConfig(upstreamConfig)
@@ -458,7 +468,6 @@
wifiAgent.disconnect()
}
-
@Test
fun testUnregisterUpstreamAfterReplacement_SameIfaceName() {
doTestUnregisterUpstreamAfterReplacement(true)
@@ -824,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/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index d7343b1..13c5cbc 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -28,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
@@ -39,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
@@ -46,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
@@ -140,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)
@@ -166,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)
@@ -178,6 +185,7 @@
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)
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 3b83c41..5c4617b 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -17,6 +17,7 @@
package com.android.server
import android.app.AlarmManager
+import android.app.AppOpsManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -42,6 +43,7 @@
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
@@ -51,9 +53,9 @@
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
@@ -75,8 +77,8 @@
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
-import java.util.function.Consumer
import java.util.function.BiConsumer
+import java.util.function.Consumer
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.fail
@@ -103,6 +105,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
@@ -128,8 +132,10 @@
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)"
+ )
}
}
@@ -147,6 +153,7 @@
it[ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION] = true
it[ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS] = true
it[ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK] = true
+ it[ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING] = true
}
fun enableFeature(f: String) = enabledFeatures.set(f, true)
fun disableFeature(f: String) = enabledFeatures.set(f, false)
@@ -176,9 +183,11 @@
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>()
@@ -226,7 +235,8 @@
context: Context,
tm: TelephonyManager,
requestRestrictedWifiEnabled: Boolean,
- listener: BiConsumer<Int, Int>
+ listener: BiConsumer<Int, Int>,
+ handler: Handler
) = if (SdkLevel.isAtLeastT()) mock<CarrierPrivilegeAuthenticator>() else null
var satelliteNetworkFallbackUidUpdate: Consumer<Set<Int>>? = null
@@ -248,8 +258,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
@@ -263,7 +277,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.
@@ -298,6 +312,19 @@
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) {
@@ -321,8 +348,12 @@
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)
+ val granted = checkMockedPermission(
+ permission,
+ Process.myPid(),
+ Process.myUid(),
+ PERMISSION_GRANTED
+ )
if (!granted.equals(PERMISSION_GRANTED)) {
throw SecurityException("[Test] permission denied: " + permission)
}
@@ -333,8 +364,12 @@
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 {
+ private fun checkMockedPermission(
+ permission: String,
+ pid: Int,
+ uid: Int,
+ default: Int
+ ): Int {
val processSpecificKey = "$permission,$pid,$uid"
return mMockedPermissions[processSpecificKey]
?: mMockedPermissions[permission] ?: default
@@ -396,16 +431,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(
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index e62ac74..0bbc34c 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -47,7 +47,6 @@
import android.net.NetworkTemplate;
import android.os.HandlerThread;
import android.os.IBinder;
-import android.os.Looper;
import android.os.Process;
import android.os.UserHandle;
import android.telephony.TelephonyManager;
@@ -127,13 +126,7 @@
mObserverHandlerThread = new HandlerThread("NetworkStatsObserversTest");
mObserverHandlerThread.start();
- final Looper observerLooper = mObserverHandlerThread.getLooper();
- mStatsObservers = new NetworkStatsObservers() {
- @Override
- protected Looper getHandlerLooperLocked() {
- return observerLooper;
- }
- };
+ mStatsObservers = new NetworkStatsObservers(mObserverHandlerThread.getLooper());
mActiveIfaces = new ArrayMap<>();
mActiveUidIfaces = new ArrayMap<>();
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 3ed51bc..3d7ad66 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -123,7 +123,6 @@
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
-import android.os.Looper;
import android.os.PowerManager;
import android.os.SimpleClock;
import android.provider.Settings;
@@ -293,7 +292,6 @@
private String mCompareStatsResult = null;
private @Mock Resources mResources;
private Boolean mIsDebuggable;
- private HandlerThread mObserverHandlerThread;
final TestDependencies mDeps = new TestDependencies();
private class MockContext extends BroadcastInterceptingContext {
@@ -377,21 +375,8 @@
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
mHandlerThread = new HandlerThread("NetworkStatsServiceTest-HandlerThread");
- // Create a separate thread for observers to run on. This thread cannot be the same
- // as the handler thread, because the observer callback is fired on this thread, and
- // it should not be blocked by client code. Additionally, creating the observers
- // object requires a looper, which can only be obtained after a thread has been started.
- mObserverHandlerThread = new HandlerThread("NetworkStatsServiceTest-ObserversThread");
- mObserverHandlerThread.start();
- final Looper observerLooper = mObserverHandlerThread.getLooper();
- final NetworkStatsObservers statsObservers = new NetworkStatsObservers() {
- @Override
- protected Looper getHandlerLooperLocked() {
- return observerLooper;
- }
- };
mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
- mClock, mSettings, mStatsFactory, statsObservers, mDeps);
+ mClock, mSettings, mStatsFactory, mDeps);
mElapsedRealtime = 0L;
@@ -589,10 +574,6 @@
mHandlerThread.quitSafely();
mHandlerThread.join();
}
- if (mObserverHandlerThread != null) {
- mObserverHandlerThread.quitSafely();
- mObserverHandlerThread.join();
- }
}
private void initWifiStats(NetworkStateSnapshot snapshot) throws Exception {
diff --git a/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
index 27e6f96..99f762d 100644
--- a/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
+++ b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
@@ -16,30 +16,35 @@
package com.android.server.net
-import android.net.NetworkStats
+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(NetworkStats.Entry::class.java)
- private val cache = TrafficStatsRateLimitCache(clock, expiryDurationMs)
+ private val entry = mock(Entry::class.java)
+ private val cache = TrafficStatsRateLimitCache(clock, expiryDurationMs, maxSize)
@Test
fun testGet_returnsEntryIfNotExpired() {
cache.put("iface", 2, entry)
- `when`(clock.millis()).thenReturn(500L) // Set clock to before expiry
+ doReturn(500L).`when`(clock).millis() // Set clock to before expiry
val result = cache.get("iface", 2)
assertEquals(entry, result)
}
@@ -47,7 +52,7 @@
@Test
fun testGet_returnsNullIfExpired() {
cache.put("iface", 2, entry)
- `when`(clock.millis()).thenReturn(2000L) // Set clock to after expiry
+ doReturn(2000L).`when`(clock).millis() // Set clock to after expiry
assertNull(cache.get("iface", 2))
}
@@ -59,8 +64,8 @@
@Test
fun testPutAndGet_retrievesCorrectEntryForDifferentKeys() {
- val entry1 = mock(NetworkStats.Entry::class.java)
- val entry2 = mock(NetworkStats.Entry::class.java)
+ val entry1 = mock(Entry::class.java)
+ val entry2 = mock(Entry::class.java)
cache.put("iface1", 2, entry1)
cache.put("iface2", 4, entry2)
@@ -71,8 +76,8 @@
@Test
fun testPut_overridesExistingEntry() {
- val entry1 = mock(NetworkStats.Entry::class.java)
- val entry2 = mock(NetworkStats.Entry::class.java)
+ 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
@@ -81,6 +86,62 @@
}
@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()
diff --git a/tests/unit/vpn-jarjar-rules.txt b/tests/unit/vpn-jarjar-rules.txt
deleted file mode 100644
index f74eab8..0000000
--- a/tests/unit/vpn-jarjar-rules.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-# Only keep classes imported by ConnectivityServiceTest
-keep com.android.server.connectivity.VpnProfileStore
diff --git a/thread/demoapp/Android.bp b/thread/demoapp/Android.bp
index fcfd469..117b4f9 100644
--- a/thread/demoapp/Android.bp
+++ b/thread/demoapp/Android.bp
@@ -34,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 846253c..f8fe422 100644
--- a/thread/framework/Android.bp
+++ b/thread/framework/Android.bp
@@ -30,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/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index 485e25d..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,6 +40,7 @@
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);
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index db761a3..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;
@@ -98,6 +100,12 @@
/** 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})
@@ -596,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 4def0fb..f699c30 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkException.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -138,8 +138,17 @@
*/
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_THREAD_DISABLED;
+ private static final int ERROR_MAX = ERROR_UNSUPPORTED_OPERATION;
private final int mErrorCode;
diff --git a/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
index e3b4e1a..43ff336 100644
--- a/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
+++ b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
@@ -16,10 +16,12 @@
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;
@@ -73,6 +75,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) {
synchronized (sPendingReceiversLock) {
sPendingReceivers.remove(this);
diff --git a/thread/service/java/com/android/server/thread/NsdPublisher.java b/thread/service/java/com/android/server/thread/NsdPublisher.java
index 440c2c3..72e3980 100644
--- a/thread/service/java/com/android/server/thread/NsdPublisher.java
+++ b/thread/service/java/com/android/server/thread/NsdPublisher.java
@@ -21,6 +21,7 @@
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;
@@ -31,15 +32,20 @@
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.ArrayDeque;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.Executor;
/**
@@ -66,6 +72,8 @@
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);
private final Deque<Runnable> mRegistrationJobs = new ArrayDeque<>();
@VisibleForTesting
@@ -197,6 +205,110 @@
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(
@@ -204,8 +316,12 @@
}
}
- /** On ot-daemon died, unregister all registrations. */
- public void onOtDaemonDied() {
+ @Override
+ public void reset() {
+ mHandler.post(this::resetInternal);
+ }
+
+ private void resetInternal() {
checkOnHandlerThread();
for (int i = 0; i < mRegistrationListeners.size(); ++i) {
try {
@@ -222,6 +338,12 @@
}
}
mRegistrationListeners.clear();
+ mRegistrationJobs.clear();
+ }
+
+ /** On ot-daemon died, reset. */
+ public void onOtDaemonDied() {
+ reset();
}
// TODO: b/323300118 - Remove this mechanism when the race condition in NsdManager is fixed.
@@ -358,4 +480,166 @@
popAndRunNext();
}
}
+
+ 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 0623b87..0b13d1b 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -40,6 +40,7 @@
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;
@@ -47,6 +48,7 @@
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_BUSY;
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;
@@ -68,6 +70,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.InetAddresses;
import android.net.LinkAddress;
@@ -85,6 +88,7 @@
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;
@@ -94,6 +98,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;
@@ -106,8 +111,10 @@
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;
@@ -115,12 +122,16 @@
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;
@@ -129,6 +140,7 @@
import java.util.Objects;
import java.util.Random;
import java.util.function.Supplier;
+import java.util.regex.Pattern;
/**
* Implementation of the {@link ThreadNetworkController} API.
@@ -143,6 +155,16 @@
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})$";
+
// Below member fields can be accessed from both the binder and handler threads
private final Context mContext;
@@ -159,7 +181,10 @@
private final InfraInterfaceController mInfraIfController;
private final NsdPublisher mNsdPublisher;
private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
+ private final ConnectivityResources mResources;
+ // 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;
@@ -174,6 +199,7 @@
private final ThreadPersistentSettings mPersistentSettings;
private final UserManager mUserManager;
private boolean mUserRestricted;
+ private boolean mForceStopOtDaemonEnabled;
private BorderRouterConfigurationParcel mBorderRouterConfig;
@@ -188,7 +214,8 @@
InfraInterfaceController infraIfController,
ThreadPersistentSettings persistentSettings,
NsdPublisher nsdPublisher,
- UserManager userManager) {
+ UserManager userManager,
+ ConnectivityResources resources) {
mContext = context;
mHandler = handler;
mNetworkProvider = networkProvider;
@@ -202,6 +229,7 @@
mPersistentSettings = persistentSettings;
mNsdPublisher = nsdPublisher;
mUserManager = userManager;
+ mResources = resources;
}
public static ThreadNetworkControllerService newInstance(
@@ -222,7 +250,8 @@
new InfraInterfaceController(),
persistentSettings,
NsdPublisher.newInstance(context, handler),
- context.getSystemService(UserManager.class));
+ context.getSystemService(UserManager.class),
+ new ConnectivityResources(context));
}
private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
@@ -279,17 +308,31 @@
.build();
}
- private void initializeOtDaemon() {
+ private void maybeInitializeOtDaemon() {
+ if (!isEnabled()) {
+ return;
+ }
+
+ Log.i(TAG, "Starting OT daemon...");
+
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;
}
@@ -298,29 +341,74 @@
if (otDaemon == null) {
throw new RemoteException("Internal error: failed to start OT daemon");
}
- otDaemon.initialize(mTunIfController.getTunFd(), isEnabled(), mNsdPublisher);
- otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
+
+ otDaemon.initialize(
+ mTunIfController.getTunFd(),
+ isEnabled(),
+ mNsdPublisher,
+ getMeshcopTxtAttributes(mResources.get()),
+ mOtDaemonCallbackProxy);
otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
mOtDaemon = otDaemon;
return mOtDaemon;
}
+ @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() {
checkOnHandlerThread();
- Log.w(TAG, "OT daemon is dead, clean up and restart it...");
+ Log.w(TAG, "OT daemon is dead, clean up...");
OperationReceiverWrapper.onOtDaemonDied();
mOtDaemonCallbackProxy.onOtDaemonDied();
mTunIfController.onOtDaemonDied();
mNsdPublisher.onOtDaemonDied();
mOtDaemon = null;
- initializeOtDaemon();
+ 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) {
@@ -332,10 +420,59 @@
requestThreadNetwork();
mUserRestricted = isThreadUserRestricted();
registerUserRestrictionsReceiver();
- initializeOtDaemon();
+ 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);
@@ -356,6 +493,8 @@
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
@@ -365,9 +504,9 @@
try {
getOtDaemon().setThreadEnabled(isEnabled, newOtStatusReceiver(receiver));
- } catch (RemoteException e) {
+ } catch (RemoteException | ThreadNetworkException e) {
Log.e(TAG, "otDaemon.setThreadEnabled failed", e);
- receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ receiver.onError(e);
}
}
@@ -424,7 +563,9 @@
/** Returns {@code true} if Thread is set enabled. */
private boolean isEnabled() {
- return !mUserRestricted && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
+ return !mForceStopOtDaemonEnabled
+ && !mUserRestricted
+ && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
}
/** Returns {@code true} if Thread has been restricted for the user. */
@@ -489,7 +630,14 @@
@Override
public void onAvailable(@NonNull Network network) {
checkOnHandlerThread();
- Log.i(TAG, "Thread network available: " + network);
+ 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
@@ -504,7 +652,7 @@
+ localNetworkInfo
+ "}");
if (localNetworkInfo.getUpstreamNetwork() == null) {
- mUpstreamNetwork = null;
+ disableBorderRouting();
return;
}
if (!localNetworkInfo.getUpstreamNetwork().equals(mUpstreamNetwork)) {
@@ -523,6 +671,7 @@
// requirement.
.clearCapabilities()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
.build(),
new ThreadNetworkCallback(),
mHandler);
@@ -604,9 +753,9 @@
try {
getOtDaemon().getChannelMasks(newChannelMasksReceiver(networkName, receiver));
- } catch (RemoteException e) {
+ } catch (RemoteException | ThreadNetworkException e) {
Log.e(TAG, "otDaemon.getChannelMasks failed", e);
- receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ receiver.onError(e);
}
}
@@ -778,6 +927,8 @@
return ERROR_ABORTED;
case OT_ERROR_BUSY:
return ERROR_BUSY;
+ case OT_ERROR_NOT_IMPLEMENTED:
+ return ERROR_UNSUPPORTED_OPERATION;
case OT_ERROR_NO_BUFS:
return ERROR_RESOURCE_EXHAUSTED;
case OT_ERROR_PARSE:
@@ -816,9 +967,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);
}
}
@@ -841,9 +992,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);
}
}
@@ -859,9 +1010,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);
}
}
@@ -883,11 +1034,18 @@
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(ERROR_INTERNAL_ERROR, "Thread stack error");
+ receiver.onError(e);
}
}
@@ -923,6 +1081,30 @@
}
}
+ @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");
+ }
+ }
+
private void enableBorderRouting(String infraIfName) {
if (mBorderRouterConfig.isBorderRoutingEnabled
&& infraIfName.equals(mBorderRouterConfig.infraInterfaceName)) {
@@ -935,24 +1117,25 @@
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);
}
}
@@ -1073,6 +1256,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}.
@@ -1105,8 +1302,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);
}
}
@@ -1146,8 +1343,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);
}
}
@@ -1166,8 +1363,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;
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkService.java b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
index 5664922..37c1cf1 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;
@@ -66,7 +68,8 @@
// PHASE_ACTIVITY_MANAGER_READY and PHASE_THIRD_PARTY_APPS_CAN_START
mCountryCode = ThreadNetworkCountryCode.newInstance(mContext, mControllerService);
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 aba4193..923f002 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -46,10 +46,13 @@
*/
public class ThreadPersistentSettings {
private static final String TAG = "ThreadPersistentSettings";
+
/** File name used for storing settings. */
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.
@@ -58,7 +61,7 @@
/******** 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);
/******** Thread persistent setting keys ***************/
@@ -210,7 +213,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);
}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 5890d26..8cdf38d 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -19,7 +19,6 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-// TODO: add this test to the CTS test suite
android_test {
name: "CtsThreadNetworkTestCases",
min_sdk_version: "33",
@@ -30,6 +29,7 @@
"src/**/*.java",
],
test_suites: [
+ "cts",
"general-tests",
"mcts-tethering",
"mts-tethering",
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 36ce4d5..0591c87 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -40,7 +40,6 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeNotNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -60,7 +59,8 @@
import android.net.thread.ThreadNetworkException;
import android.net.thread.ThreadNetworkManager;
import android.net.thread.utils.TapTestNetworkTracker;
-import android.os.Build;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
import android.os.HandlerThread;
import android.os.OutcomeReceiver;
@@ -69,16 +69,12 @@
import androidx.test.filters.LargeTest;
import com.android.net.module.util.ArrayTrackRecord;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.FunctionalUtils.ThrowingRunnable;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
-import org.junit.runner.RunWith;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
@@ -99,8 +95,7 @@
/** 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 JOIN_TIMEOUT_MILLIS = 30 * 1000;
private static final int LEAVE_TIMEOUT_MILLIS = 2_000;
@@ -109,11 +104,12 @@
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;
@@ -126,14 +122,10 @@
@Before
public void setUp() throws Exception {
- ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
- if (manager != null) {
- mController = manager.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(mController);
+ mController =
+ mContext.getSystemService(ThreadNetworkManager.class)
+ .getAllThreadNetworkControllers()
+ .get(0);
mGrantedPermissions = new HashSet<String>();
mExecutor = Executors.newSingleThreadExecutor();
@@ -146,9 +138,6 @@
@After
public void tearDown() throws Exception {
- if (mController == null) {
- return;
- }
dropAllPermissions();
leaveAndWait(mController);
tearDownTestNetwork();
@@ -881,7 +870,7 @@
discoverForServiceLost(MESHCOP_SERVICE_TYPE, serviceLostFuture);
setEnabledAndWait(mController, false);
try {
- serviceLostFuture.get(SERVICE_DISCOVERY_TIMEOUT_MILLIS, MILLISECONDS);
+ 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.
@@ -889,7 +878,9 @@
mNsdManager.stopServiceDiscovery(listener);
}
- assertThrows(TimeoutException.class, () -> discoverService(MESHCOP_SERVICE_TYPE));
+ assertThrows(
+ TimeoutException.class,
+ () -> discoverService(MESHCOP_SERVICE_TYPE, SERVICE_LOST_TIMEOUT_MILLIS));
}
private static void dropAllPermissions() {
@@ -1107,6 +1098,12 @@
// Return the first discovered service instance.
private NsdServiceInfo discoverService(String serviceType) throws Exception {
+ return discoverService(serviceType, SERVICE_DISCOVERY_TIMEOUT_MILLIS);
+ }
+
+ // Return the first discovered service instance.
+ private NsdServiceInfo discoverService(String serviceType, int timeoutMilliseconds)
+ throws Exception {
CompletableFuture<NsdServiceInfo> serviceInfoFuture = new CompletableFuture<>();
NsdManager.DiscoveryListener listener =
new DefaultDiscoveryListener() {
@@ -1117,7 +1114,7 @@
};
mNsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener);
try {
- serviceInfoFuture.get(SERVICE_DISCOVERY_TIMEOUT_MILLIS, MILLISECONDS);
+ serviceInfoFuture.get(timeoutMilliseconds, MILLISECONDS);
} finally {
mNsdManager.stopServiceDiscovery(listener);
}
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index d84cd20..9b1c338 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -17,14 +17,10 @@
package android.net.thread;
import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
-import static android.Manifest.permission.NETWORK_SETTINGS;
-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.RESTART_JOIN_TIMEOUT;
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.isSimulatedThreadRadioSupported;
import static android.net.thread.utils.IntegrationTestUtils.isToIpv6Destination;
import static android.net.thread.utils.IntegrationTestUtils.newPacketReader;
import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
@@ -33,21 +29,17 @@
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.DeviceInfoUtils.isKernelVersionAtLeast;
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.util.concurrent.MoreExecutors.directExecutor;
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.Assume.assumeNotNull;
-import static org.junit.Assume.assumeTrue;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.Objects.requireNonNull;
import android.content.Context;
import android.net.InetAddresses;
@@ -56,6 +48,11 @@
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;
@@ -68,6 +65,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -75,28 +73,16 @@
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.CompletableFuture;
-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 ThreadNetworkController mController;
- private OtDaemonController mOtCtl;
- private HandlerThread mHandlerThread;
- private Handler mHandler;
- private TestNetworkTracker mInfraNetworkTracker;
- private List<FullThreadDevice> mFtds;
- private TapPacketReader mInfraNetworkReader;
- private InfraNetworkDevice mInfraDevice;
-
private static final int NUM_FTD = 2;
- private static final String KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED = "5.15.0";
private static final Inet6Address GROUP_ADDR_SCOPE_5 =
(Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
private static final Inet6Address GROUP_ADDR_SCOPE_4 =
@@ -115,17 +101,21 @@
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 {
- assumeTrue(isSimulatedThreadRadioSupported());
- final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
- if (manager != null) {
- mController = manager.getAllThreadNetworkControllers().get(0);
- }
-
- // Run the tests on only devices where the Thread feature is available
- assumeNotNull(mController);
-
// TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
mOtCtl = new OtDaemonController();
mOtCtl.factoryReset();
@@ -136,13 +126,12 @@
mFtds = new ArrayList<>();
setUpInfraNetwork();
-
- // BR forms a network.
- startBrLeader();
+ mController.setEnabledAndWait(true);
+ mController.joinAndWait(DEFAULT_DATASET);
// Creates a infra network device.
mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
- startInfraDevice();
+ startInfraDeviceAndWaitForOnLinkAddr();
// Create Ftds
for (int i = 0; i < NUM_FTD; ++i) {
@@ -152,20 +141,8 @@
@After
public void tearDown() throws Exception {
- if (mController == null) {
- return;
- }
-
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- NETWORK_SETTINGS,
- () -> {
- CountDownLatch latch = new CountDownLatch(2);
- mController.setTestNetworkAsUpstream(
- null, directExecutor(), v -> latch.countDown());
- mController.leave(directExecutor(), v -> latch.countDown());
- latch.await(10, TimeUnit.SECONDS);
- });
+ mController.setTestNetworkAsUpstreamAndWait(null);
+ mController.leaveAndWait();
tearDownInfraNetwork();
mHandlerThread.quitSafely();
@@ -178,7 +155,7 @@
}
@Test
- public void unicastRouting_infraDevicePingTheadDeviceOmr_replyReceived() throws Exception {
+ public void unicastRouting_infraDevicePingThreadDeviceOmr_replyReceived() throws Exception {
/*
* <pre>
* Topology:
@@ -188,22 +165,69 @@
* </pre>
*/
- // Let ftd join the network.
FullThreadDevice ftd = mFtds.get(0);
startFtdChild(ftd);
- // Infra device sends an echo request to FTD's OMR.
mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
// Infra device receives an echo reply sent by FTD.
- assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, null /* srcAddress */));
+ 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 {
- assumeTrue(isSimulatedThreadRadioSupported());
-
/*
* <pre>
* Topology:
@@ -213,19 +237,10 @@
* </pre>
*/
- // BR forms a network.
- CompletableFuture<Void> joinFuture = new CompletableFuture<>();
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
- joinFuture.get(RESTART_JOIN_TIMEOUT.toMillis(), MILLISECONDS);
-
- // Creates a Full Thread Device (FTD) and lets it join the network.
FullThreadDevice ftd = mFtds.get(0);
startFtdChild(ftd);
- Inet6Address ftdOmr = ftd.getOmrAddress();
- Inet6Address ftdMlEid = ftd.getMlEid();
- assertNotNull(ftdMlEid);
+ Inet6Address ftdOmr = requireNonNull(ftd.getOmrAddress());
+ Inet6Address ftdMlEid = requireNonNull(ftd.getMlEid());
ftd.udpBind(ftdOmr, 12345);
sendUdpMessage(ftdOmr, 12345, "aaaaaaaa");
@@ -237,9 +252,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_ftdSubscribedMulticastAddress_infraLinkJoinsMulticastGroup()
throws Exception {
- assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
/*
* <pre>
* Topology:
@@ -258,10 +273,10 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void
multicastRouting_ftdSubscribedScope3MulticastAddress_infraLinkNotJoinMulticastGroup()
throws Exception {
- assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
/*
* <pre>
* Topology:
@@ -280,9 +295,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_ftdSubscribedMulticastAddress_canPingfromInfraLink()
throws Exception {
- assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
/*
* <pre>
* Topology:
@@ -302,9 +317,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_inboundForwarding_afterBrRejoinFtdRepliesSubscribedAddress()
throws Exception {
- assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
// 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
@@ -312,9 +327,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_ftdSubscribedScope3MulticastAddress_cannotPingfromInfraLink()
throws Exception {
- assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
/*
* <pre>
* Topology:
@@ -334,9 +349,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_ftdNotSubscribedMulticastAddress_cannotPingFromInfraDevice()
throws Exception {
- assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
/*
* <pre>
* Topology:
@@ -355,9 +370,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_multipleFtdsSubscribedDifferentAddresses_canPingFromInfraDevice()
throws Exception {
- assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
/*
* <pre>
* Topology:
@@ -391,9 +406,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_multipleFtdsSubscribedSameAddress_canPingFromInfraDevice()
throws Exception {
- assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
/*
* <pre>
* Topology:
@@ -426,8 +441,8 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_outboundForwarding_scopeLargerThan3IsForwarded() throws Exception {
- assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
/*
* <pre>
* Topology:
@@ -451,9 +466,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_outboundForwarding_scopeSmallerThan4IsNotForwarded()
throws Exception {
- assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
/*
* <pre>
* Topology:
@@ -474,8 +489,8 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_outboundForwarding_llaToScope4IsNotForwarded() throws Exception {
- assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
/*
* <pre>
* Topology:
@@ -490,15 +505,15 @@
Inet6Address ftdLla = ftd.getLinkLocalAddress();
assertNotNull(ftdLla);
- ftd.ping(GROUP_ADDR_SCOPE_4, ftdLla, 100 /* size */, 1 /* count */);
+ 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 {
- assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
/*
* <pre>
* Topology:
@@ -514,7 +529,7 @@
assertFalse(ftdMlas.isEmpty());
for (Inet6Address ftdMla : ftdMlas) {
- ftd.ping(GROUP_ADDR_SCOPE_4, ftdMla, 100 /* size */, 1 /* count */);
+ ftd.ping(GROUP_ADDR_SCOPE_4, ftdMla);
assertNull(
pollForPacketOnInfraNetwork(
@@ -523,9 +538,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_infraNetworkSwitch_ftdRepliesToSubscribedAddress()
throws Exception {
- assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
/*
* <pre>
* Topology:
@@ -544,7 +559,7 @@
tearDownInfraNetwork();
setUpInfraNetwork();
mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
- startInfraDevice();
+ startInfraDeviceAndWaitForOnLinkAddr();
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
@@ -552,8 +567,8 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_infraNetworkSwitch_outboundPacketIsForwarded() throws Exception {
- assumeTrue(isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED));
/*
* <pre>
* Topology:
@@ -571,7 +586,7 @@
tearDownInfraNetwork();
setUpInfraNetwork();
mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
- startInfraDevice();
+ startInfraDeviceAndWaitForOnLinkAddr();
ftd.ping(GROUP_ADDR_SCOPE_4);
@@ -579,38 +594,21 @@
pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
}
- private void setUpInfraNetwork() {
+ private void setUpInfraNetwork() throws Exception {
mInfraNetworkTracker =
runAsShell(
MANAGE_TEST_NETWORKS,
() ->
initTestNetwork(
mContext, new LinkProperties(), 5000 /* timeoutMs */));
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- NETWORK_SETTINGS,
- () -> {
- CompletableFuture<Void> future = new CompletableFuture<>();
- mController.setTestNetworkAsUpstream(
- mInfraNetworkTracker.getTestIface().getInterfaceName(),
- directExecutor(),
- future::complete);
- future.get(5, TimeUnit.SECONDS);
- });
+ mController.setTestNetworkAsUpstreamAndWait(
+ mInfraNetworkTracker.getTestIface().getInterfaceName());
}
private void tearDownInfraNetwork() {
runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
}
- private void startBrLeader() throws Exception {
- CompletableFuture<Void> joinFuture = new CompletableFuture<>();
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
- joinFuture.get(RESTART_JOIN_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
- }
-
private void startFtdChild(FullThreadDevice ftd) throws Exception {
ftd.factoryReset();
ftd.joinNetwork(DEFAULT_DATASET);
@@ -620,7 +618,7 @@
assertNotNull(ftdOmr);
}
- private void startInfraDevice() throws Exception {
+ private void startInfraDeviceAndWaitForOnLinkAddr() throws Exception {
mInfraDevice =
new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), mInfraNetworkReader);
mInfraDevice.runSlaac(Duration.ofSeconds(60));
diff --git a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
index 5f1f76a..491331c 100644
--- a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -16,31 +16,23 @@
package android.net.thread;
-import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.net.InetAddresses.parseNumericAddress;
-import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+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.RESTART_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.isSimulatedThreadRadioSupported;
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.android.testutils.TestPermissionUtil.runAsShell;
-
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.junit.Assert.assertThrows;
-import static org.junit.Assume.assumeNotNull;
-import static org.junit.Assume.assumeTrue;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
import android.content.Context;
import android.net.nsd.NsdManager;
@@ -48,6 +40,10 @@
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;
@@ -59,6 +55,7 @@
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;
@@ -70,22 +67,18 @@
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
@Ignore("TODO: b/328527773 - enable the test when it's stable")
public class ServiceDiscoveryTest {
private static final String TAG = ServiceDiscoveryTest.class.getSimpleName();
private static final int NUM_FTD = 3;
- private final Context mContext = ApplicationProvider.getApplicationContext();
-
- private HandlerThread mHandlerThread;
- private ThreadNetworkController mController;
- private NsdManager mNsdManager;
- private TapTestNetworkTracker mTestNetworkTracker;
- private List<FullThreadDevice> mFtds;
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
private static final byte[] DEFAULT_DATASET_TLVS =
@@ -101,26 +94,21 @@
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 List<FullThreadDevice> mFtds;
+ private List<RegistrationListener> mRegistrationListeners = new ArrayList<>();
+
@Before
public void setUp() throws Exception {
- final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
- if (manager != null) {
- mController = manager.getAllThreadNetworkControllers().get(0);
- }
-
- // Run the tests on only devices where the Thread feature is available.
- assumeNotNull(mController);
-
- // Run the tests only when the device uses simulated Thread radio.
- assumeTrue(isSimulatedThreadRadioSupported());
-
- // BR forms a network.
- CompletableFuture<Void> joinFuture = new CompletableFuture<>();
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
- joinFuture.get(RESTART_JOIN_TIMEOUT.toMillis(), MILLISECONDS);
-
+ mController.joinAndWait(DEFAULT_DATASET);
mNsdManager = mContext.getSystemService(NsdManager.class);
mHandlerThread = new HandlerThread(TAG);
@@ -128,17 +116,8 @@
mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper());
assertThat(mTestNetworkTracker).isNotNull();
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- NETWORK_SETTINGS,
- () -> {
- CompletableFuture<Void> future = new CompletableFuture<>();
- mController.setTestNetworkAsUpstream(
- mTestNetworkTracker.getInterfaceName(),
- directExecutor(),
- v -> future.complete(null));
- future.get(5, SECONDS);
- });
+ 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.
mFtds = new ArrayList<>();
@@ -151,11 +130,8 @@
@After
public void tearDown() throws Exception {
- if (mController == null) {
- return;
- }
- if (!isSimulatedThreadRadioSupported()) {
- return;
+ for (RegistrationListener listener : mRegistrationListeners) {
+ unregisterService(listener);
}
for (FullThreadDevice ftd : mFtds) {
// Clear registered SRP hosts and services
@@ -171,18 +147,8 @@
mHandlerThread.quitSafely();
mHandlerThread.join();
}
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- NETWORK_SETTINGS,
- () -> {
- CompletableFuture<Void> setUpstreamFuture = new CompletableFuture<>();
- CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
- mController.setTestNetworkAsUpstream(
- null, directExecutor(), v -> setUpstreamFuture.complete(null));
- mController.leave(directExecutor(), v -> leaveFuture.complete(null));
- setUpstreamFuture.get(5, SECONDS);
- leaveFuture.get(5, SECONDS);
- });
+ mController.setTestNetworkAsUpstreamAndWait(null);
+ mController.leaveAndWait();
}
@Test
@@ -343,6 +309,187 @@
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
+ 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) {
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index 580a83a..bfded1d 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -16,31 +16,22 @@
package android.net.thread;
-import static android.Manifest.permission.NETWORK_SETTINGS;
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.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
-import static android.net.thread.utils.IntegrationTestUtils.LEAVE_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
-import static android.net.thread.utils.IntegrationTestUtils.waitForStateAnyOf;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
-import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-import static org.junit.Assume.assumeNotNull;
-
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-
-import android.annotation.Nullable;
import android.content.Context;
-import android.net.thread.ThreadNetworkController.StateCallback;
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;
@@ -49,21 +40,18 @@
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.util.List;
-import java.util.concurrent.CompletableFuture;
/** Tests for E2E Android Thread integration with ot-daemon, ConnectivityService, etc.. */
@LargeTest
+@RequiresThreadFeature
@RunWith(AndroidJUnit4.class)
public class ThreadIntegrationTest {
- private final Context mContext = ApplicationProvider.getApplicationContext();
- private ThreadNetworkController mController;
- private OtDaemonController mOtCtl;
-
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
private static final byte[] DEFAULT_DATASET_TLVS =
base16().decode(
@@ -75,18 +63,17 @@
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;
+
@Before
public void setUp() throws Exception {
- final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
- if (manager != null) {
- mController = manager.getAllThreadNetworkControllers().get(0);
- }
-
- // Run the tests on only devices where the Thread feature is available
- assumeNotNull(mController);
-
mOtCtl = new OtDaemonController();
- leaveAndWait(mController);
+ mController.leaveAndWait();
// TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
mOtCtl.factoryReset();
@@ -94,57 +81,56 @@
@After
public void tearDown() throws Exception {
- if (mController == null) {
- return;
- }
-
- setTestUpStreamNetworkAndWait(mController, null);
- leaveAndWait(mController);
+ mController.setTestNetworkAsUpstreamAndWait(null);
+ mController.leaveAndWait();
}
@Test
public void otDaemonRestart_notJoinedAndStopped_deviceRoleIsStopped() throws Exception {
- leaveAndWait(mController);
+ mController.leaveAndWait();
runShellCommand("stop ot-daemon");
// TODO(b/323331973): the sleep is needed to workaround the race conditions
SystemClock.sleep(200);
- waitForStateAnyOf(mController, List.of(DEVICE_ROLE_STOPPED), CALLBACK_TIMEOUT);
+ mController.waitForRole(DEVICE_ROLE_STOPPED, CALLBACK_TIMEOUT);
}
@Test
- public void otDaemonRestart_JoinedNetworkAndStopped_autoRejoined() throws Exception {
- joinAndWait(mController, DEFAULT_DATASET);
+ public void otDaemonRestart_JoinedNetworkAndStopped_autoRejoinedAndTunIfStateConsistent()
+ throws Exception {
+ mController.joinAndWait(DEFAULT_DATASET);
runShellCommand("stop ot-daemon");
- waitForStateAnyOf(mController, List.of(DEVICE_ROLE_DETACHED), CALLBACK_TIMEOUT);
- waitForStateAnyOf(mController, List.of(DEVICE_ROLE_LEADER), RESTART_JOIN_TIMEOUT);
+ 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 {
- joinAndWait(mController, DEFAULT_DATASET);
+ mController.joinAndWait(DEFAULT_DATASET);
mOtCtl.factoryReset();
- assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+ assertThat(mController.getDeviceRole()).isEqualTo(DEVICE_ROLE_STOPPED);
}
@Test
public void otDaemonFactoryReset_addressesRemoved() throws Exception {
- joinAndWait(mController, DEFAULT_DATASET);
+ mController.joinAndWait(DEFAULT_DATASET);
mOtCtl.factoryReset();
- String ifconfig = runShellCommand("ifconfig thread-wpan");
+ String ifconfig = runShellCommand("ifconfig thread-wpan");
assertThat(ifconfig).doesNotContain("inet6 addr");
}
@Test
public void tunInterface_joinedNetwork_otAddressesAddedToTunInterface() throws Exception {
- joinAndWait(mController, DEFAULT_DATASET);
+ mController.joinAndWait(DEFAULT_DATASET);
String ifconfig = runShellCommand("ifconfig thread-wpan");
List<Inet6Address> otAddresses = mOtCtl.getAddresses();
@@ -156,46 +142,4 @@
// TODO (b/323300829): add more tests for integration with linux platform and
// ConnectivityService
-
- private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
- CompletableFuture<Integer> future = new CompletableFuture<>();
- StateCallback callback = future::complete;
- controller.registerStateCallback(directExecutor(), callback);
- try {
- return future.get(CALLBACK_TIMEOUT.toMillis(), MILLISECONDS);
- } finally {
- controller.unregisterStateCallback(callback);
- }
- }
-
- private static void joinAndWait(
- ThreadNetworkController controller, ActiveOperationalDataset activeDataset)
- throws Exception {
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> controller.join(activeDataset, directExecutor(), result -> {}));
- waitForStateAnyOf(controller, List.of(DEVICE_ROLE_LEADER), RESTART_JOIN_TIMEOUT);
- }
-
- private static void leaveAndWait(ThreadNetworkController controller) throws Exception {
- CompletableFuture<Void> future = new CompletableFuture<>();
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> controller.leave(directExecutor(), future::complete));
- future.get(LEAVE_TIMEOUT.toMillis(), MILLISECONDS);
- }
-
- private static void setTestUpStreamNetworkAndWait(
- ThreadNetworkController controller, @Nullable String networkInterfaceName)
- throws Exception {
- CompletableFuture<Void> future = new CompletableFuture<>();
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- NETWORK_SETTINGS,
- () -> {
- controller.setTestNetworkAsUpstream(
- networkInterfaceName, directExecutor(), future::complete);
- });
- future.get(CALLBACK_TIMEOUT.toMillis(), MILLISECONDS);
- }
}
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
index 6306a65..f7bb9ff 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -24,6 +24,7 @@
import android.net.InetAddresses;
import android.net.IpPrefix;
+import android.net.nsd.NsdServiceInfo;
import android.net.thread.ActiveOperationalDataset;
import com.google.errorprone.annotations.FormatMethod;
@@ -34,6 +35,7 @@
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;
@@ -52,6 +54,13 @@
* 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;
@@ -320,6 +329,55 @@
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 {
@@ -339,7 +397,36 @@
executeCommand("ipmaddr add " + address.getHostAddress());
}
- public void ping(Inet6Address address, Inet6Address source, int size, int count) {
+ 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()))
@@ -348,14 +435,16 @@
+ " "
+ size
+ " "
- + count;
+ + count
+ + " "
+ + interval
+ + " "
+ + hopLimit
+ + " "
+ + timeout;
executeCommand(cmd);
}
- public void ping(Inet6Address address) {
- ping(address, null, 100 /* size */, 1 /* count */);
- }
-
@FormatMethod
private List<String> executeCommand(String commandFormat, Object... args) {
return executeCommand(String.format(commandFormat, args));
@@ -416,4 +505,45 @@
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/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
index 6e70d24..2237e65 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
@@ -31,7 +31,6 @@
import android.net.thread.ThreadNetworkController;
import android.os.Handler;
import android.os.SystemClock;
-import android.os.SystemProperties;
import androidx.annotation.NonNull;
@@ -77,12 +76,6 @@
private IntegrationTestUtils() {}
- /** Returns whether the device supports simulated Thread radio. */
- public static boolean isSimulatedThreadRadioSupported() {
- // The integration test uses SIMULATION Thread radio so that it only supports CuttleFish.
- return SystemProperties.get("ro.product.model").startsWith("Cuttlefish");
- }
-
/**
* Waits for the given {@link Supplier} to be true until given timeout.
*
diff --git a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
index 4a06fe8..ade0669 100644
--- a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
+++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
@@ -62,6 +62,18 @@
.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 = executeCommand("ipaddr mleid").split("\n")[0].trim();
+ return (Inet6Address) InetAddresses.parseNumericAddress(addressStr);
+ }
+
public String executeCommand(String cmd) {
return SystemUtil.runShellCommand(OT_CTL + " " + cmd);
}
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/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
index f62b437..5908c20 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkExceptionTest.java
@@ -32,6 +32,6 @@
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(13, "13"));
+ assertThrows(IllegalArgumentException.class, () -> new ThreadNetworkException(14, "14"));
}
}
diff --git a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
index 54e89b1..8886c73 100644
--- a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
@@ -23,30 +23,38 @@
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;
@@ -56,6 +64,8 @@
@Mock private INsdStatusReceiver mRegistrationReceiver;
@Mock private INsdStatusReceiver mUnregistrationReceiver;
+ @Mock private INsdDiscoverServiceCallback mDiscoverServiceCallback;
+ @Mock private INsdResolveServiceCallback mResolveServiceCallback;
private TestLooper mTestLooper;
private NsdPublisher mNsdPublisher;
@@ -468,7 +478,166 @@
}
@Test
- public void onOtDaemonDied_unregisterAll() {
+ 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));
@@ -540,7 +709,7 @@
actualRegistrationListenerCaptor.getAllValues().get(1);
actualListener3.onServiceRegistered(actualServiceInfoCaptor.getValue());
- mNsdPublisher.onOtDaemonDied();
+ mNsdPublisher.reset();
mTestLooper.dispatchAll();
verify(mMockNsdManager, times(1)).unregisterService(actualListener1);
@@ -548,6 +717,17 @@
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();
@@ -570,6 +750,30 @@
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.
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 60a5f2b..0c7d086 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -21,6 +21,7 @@
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;
@@ -37,6 +38,7 @@
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;
@@ -47,6 +49,7 @@
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;
@@ -65,6 +68,9 @@
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;
@@ -72,7 +78,9 @@
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;
@@ -110,6 +118,11 @@
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;
@@ -119,6 +132,9 @@
@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;
@@ -146,6 +162,14 @@
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(
mContext,
@@ -157,7 +181,8 @@
mMockInfraIfController,
mMockPersistentSettings,
mMockNsdPublisher,
- mMockUserManager);
+ mMockUserManager,
+ mConnectivityResources);
mService.setTestNetworkAgent(mMockNetworkAgent);
}
@@ -174,6 +199,93 @@
}
@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
public void join_otDaemonRemoteFailure_returnsInternalError() throws Exception {
mService.initialize();
final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
@@ -204,13 +316,13 @@
}
@Test
- public void userRestriction_initWithUserRestricted_threadIsDisabled() {
+ public void userRestriction_initWithUserRestricted_otDaemonNotStarted() {
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
mService.initialize();
mTestLooper.dispatchAll();
- assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+ assertThat(mFakeOtDaemon.isInitialized()).isFalse();
}
@Test
@@ -332,4 +444,70 @@
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/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
index 49b002a..9406a2f 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -17,7 +17,9 @@
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;
@@ -30,13 +32,13 @@
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 java.io.ByteArrayOutputStream;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -44,6 +46,10 @@
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
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 9216b5b..2c2ed14 100644
--- a/tools/Android.bp
+++ b/tools/Android.bp
@@ -83,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