Merge "Change error log to warning for removing multicast vif" into main
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 19dd492..2878f79 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -102,15 +102,14 @@
"dscpPolicy.o",
"netd.o",
"offload.o",
- "offload@mainline.o",
"test.o",
- "test@mainline.o",
],
apps: [
"ServiceConnectivityResources",
],
prebuilts: [
"current_sdkinfo",
+ "netbpfload.31rc",
"netbpfload.33rc",
"netbpfload.35rc",
"ot-daemon.34rc",
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index b50831d..e37c5db 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -708,8 +708,7 @@
// If tethering is already enabled with a different request,
// disable before re-enabling.
if (unfinishedRequest != null && !unfinishedRequest.equalsIgnoreUidPackage(request)) {
- enableTetheringInternal(false /* disabled */, unfinishedRequest, null);
- mEntitlementMgr.stopProvisioningIfNeeded(type);
+ stopTetheringInternal(type);
}
mPendingTetheringRequests.put(type, request);
@@ -1152,7 +1151,10 @@
} catch (RemoteException e) { }
}
- final TetheringRequest request = createLegacyGlobalScopeTetheringRequest(type);
+ TetheringRequest request = getPendingTetheringRequest(type);
+ if (request == null) {
+ request = createLegacyGlobalScopeTetheringRequest(type);
+ }
int result = tetherInternal(request, iface);
switch (type) {
case TETHERING_WIFI:
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 e1c2db9..50ecfe1 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -2879,6 +2879,44 @@
}
@Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRequestStaticIpLegacyTether() throws Exception {
+ initTetheringOnTestThread();
+
+ // Call startTethering with static ip
+ final LinkAddress serverLinkAddr = new LinkAddress("192.168.0.123/24");
+ final LinkAddress clientLinkAddr = new LinkAddress("192.168.0.42/24");
+ final String serverAddr = "192.168.0.123";
+ final int clientAddrParceled = 0xc0a8002a;
+ final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
+ ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
+ when(mWifiManager.startTetheredHotspot(any())).thenReturn(true);
+ mTethering.startTethering(createTetheringRequest(TETHERING_WIFI,
+ serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL, null),
+ TEST_CALLER_PKG, null);
+ mLooper.dispatchAll();
+ verify(mWifiManager, times(1)).startTetheredHotspot(any());
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+
+ // Call legacyTether on the interface before the link layer event comes back.
+ // This happens, for example, in pre-T bluetooth tethering: Settings calls startTethering,
+ // and then the bluetooth code calls the tether() API.
+ final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
+ mTethering.legacyTether(TEST_WLAN_IFNAME, tetherResult);
+ mLooper.dispatchAll();
+ tetherResult.assertHasResult();
+
+ // Verify that the static ip set in startTethering is used
+ verify(mNetd).interfaceSetCfg(argThat(cfg -> serverAddr.equals(cfg.ipv4Addr)));
+ verify(mIpServerDependencies, times(1)).makeDhcpServer(any(), dhcpParamsCaptor.capture(),
+ any());
+ final DhcpServingParamsParcel params = dhcpParamsCaptor.getValue();
+ assertEquals(serverAddr, intToInet4AddressHTH(params.serverAddr).getHostAddress());
+ assertEquals(24, params.serverAddrPrefixLength);
+ assertEquals(clientAddrParceled, params.singleClientAddr);
+ }
+
+ @Test
public void testUpstreamNetworkChanged() throws Exception {
initTetheringOnTestThread();
final InOrder inOrder = inOrder(mNotificationUpdater);
diff --git a/bpf/dns_helper/DnsBpfHelper.cpp b/bpf/dns_helper/DnsBpfHelper.cpp
index 0719ade..cf2fa2b 100644
--- a/bpf/dns_helper/DnsBpfHelper.cpp
+++ b/bpf/dns_helper/DnsBpfHelper.cpp
@@ -32,12 +32,44 @@
} \
} while (0)
+// copied from BpfHandler.cpp
+static bool mainlineNetBpfLoadDone() {
+ return !access("/sys/fs/bpf/netd_shared/mainline_done", F_OK);
+}
+
+// copied from BpfHandler.cpp
+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.mdnsd_netbpfload", "stopped", std::chrono::seconds(delay))
+ && mainlineNetBpfLoadDone()) return;
+ LOG(WARNING) << "Waited " << delay << "s for init.svc.mdnsd_netbpfload=stopped, still waiting.";
+ }
+}
+
base::Result<void> DnsBpfHelper::init() {
- if (!android::modules::sdklevel::IsAtLeastT()) {
- LOG(ERROR) << __func__ << ": Unsupported before Android T.";
+ if (!android::modules::sdklevel::IsAtLeastS()) {
+ LOG(ERROR) << __func__ << ": Unsupported before Android S.";
return base::Error(EOPNOTSUPP);
}
+ if (!android::modules::sdklevel::IsAtLeastT()) {
+ LOG(INFO) << "performing Android S mainline NetBpfload magic!";
+ if (!mainlineNetBpfLoadDone()) {
+ // We're on S/Sv2 & it's the first time netd is starting up (unless crashlooping)
+ if (!base::SetProperty("ctl.start", "mdnsd_netbpfload")) {
+ LOG(ERROR) << "Failed to set property ctl.start=mdnsd_netbpfload, see dmesg for reason.";
+ return base::Error(ENOEXEC);
+ }
+
+ LOG(INFO) << "Waiting for Networking BPF programs";
+ waitForNetProgsLoaded();
+ LOG(INFO) << "Networking BPF programs are loaded";
+ }
+ return {};
+ }
+
RETURN_IF_RESULT_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH));
RETURN_IF_RESULT_NOT_OK(mUidOwnerMap.init(UID_OWNER_MAP_PATH));
RETURN_IF_RESULT_NOT_OK(mDataSaverEnabledMap.init(DATA_SAVER_ENABLED_MAP_PATH));
diff --git a/bpf/headers/include/bpf_helpers.h b/bpf/headers/include/bpf_helpers.h
index 6a0e5a8..9d6b6f6 100644
--- a/bpf/headers/include/bpf_helpers.h
+++ b/bpf/headers/include/bpf_helpers.h
@@ -46,12 +46,12 @@
#define BPFLOADER_U_QPR2_VERSION 41u
#define BPFLOADER_PLATFORM_VERSION BPFLOADER_U_QPR2_VERSION
-// Android Mainline - this bpfloader should eventually go back to T (or even S)
+// Android Mainline BpfLoader when running on Android S (sdk=31)
// Note: this value (and the following +1u's) are hardcoded in NetBpfLoad.cpp
-#define BPFLOADER_MAINLINE_VERSION 42u
+#define BPFLOADER_MAINLINE_S_VERSION 42u
// Android Mainline BpfLoader when running on Android T (sdk=33)
-#define BPFLOADER_MAINLINE_T_VERSION (BPFLOADER_MAINLINE_VERSION + 1u)
+#define BPFLOADER_MAINLINE_T_VERSION (BPFLOADER_MAINLINE_S_VERSION + 1u)
// Android Mainline BpfLoader when running on Android U (sdk=34)
#define BPFLOADER_MAINLINE_U_VERSION (BPFLOADER_MAINLINE_T_VERSION + 1u)
@@ -112,7 +112,7 @@
unsigned int _bpfloader_max_ver SECTION("bpfloader_max_ver") = BPFLOADER_MAX_VER; \
size_t _size_of_bpf_map_def SECTION("size_of_bpf_map_def") = sizeof(struct bpf_map_def); \
size_t _size_of_bpf_prog_def SECTION("size_of_bpf_prog_def") = sizeof(struct bpf_prog_def); \
- unsigned _btf_min_bpfloader_ver SECTION("btf_min_bpfloader_ver") = BPFLOADER_MAINLINE_VERSION; \
+ unsigned _btf_min_bpfloader_ver SECTION("btf_min_bpfloader_ver") = BPFLOADER_MAINLINE_S_VERSION; \
unsigned _btf_user_min_bpfloader_ver SECTION("btf_user_min_bpfloader_ver") = 0xFFFFFFFFu; \
char _license[] SECTION("license") = (NAME)
diff --git a/bpf/loader/Android.bp b/bpf/loader/Android.bp
index b08913a..780fe20 100644
--- a/bpf/loader/Android.bp
+++ b/bpf/loader/Android.bp
@@ -56,11 +56,22 @@
installable: false,
}
-// Versioned netbpfload init rc: init system will process it only on api T/33+ devices
+// Versioned netbpfload init rc: init system will process it only on api R/30 S/31 Sv2/32 devices
// Note: R[30] S[31] Sv2[32] T[33] U[34] V[35])
//
// For details of versioned rc files see:
// https://android.googlesource.com/platform/system/core/+/HEAD/init/README.md#versioned-rc-files-within-apexs
+//
+// However, .Xrc versioning doesn't work on S, so we use unversioned, and thus *do* trigger on R,
+// luckily nothing ever uses the new service on R, so you can think of it as being S/Sv2 only
+prebuilt_etc {
+ name: "netbpfload.31rc",
+ src: "netbpfload.31rc",
+ filename: "netbpfload.rc", // intentional: .31rc wouldn't take effect on S
+ installable: false,
+}
+
+// Versioned netbpfload init rc: init system will process it only on api T/33+ devices
prebuilt_etc {
name: "netbpfload.33rc",
src: "netbpfload.33rc",
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index 9486e75..bad506f 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -60,7 +60,7 @@
#include "bpf_map_def.h"
// The following matches bpf_helpers.h, which is only for inclusion in bpf code
-#define BPFLOADER_MAINLINE_VERSION 42u
+#define BPFLOADER_MAINLINE_S_VERSION 42u
#define BPFLOADER_MAINLINE_25Q2_VERSION 47u
using android::base::EndsWith;
@@ -122,6 +122,7 @@
struct Location {
const char* const dir = "";
const char* const prefix = "";
+ const bool t_plus = true;
};
// Returns the build type string (from ro.build.type).
@@ -1187,7 +1188,7 @@
ret = readCodeSections(elfFile, cs);
// BPF .o's with no programs are only supported by mainline netbpfload,
// make sure .o's targeting non-mainline (ie. S) bpfloader don't show up.
- if (ret == -ENOENT && bpfLoaderMinVer >= BPFLOADER_MAINLINE_VERSION)
+ if (ret == -ENOENT && bpfLoaderMinVer >= BPFLOADER_MAINLINE_S_VERSION)
return 0;
if (ret) {
ALOGE("Couldn't read all code sections in %s", elfPath);
@@ -1216,8 +1217,9 @@
const Location locations[] = {
// S+ Tethering mainline module (network_stack): tether offload
{
- .dir = BPFROOT "/",
+ .dir = BPFROOT "/tethering/",
.prefix = "tethering/",
+ .t_plus = false,
},
// T+ Tethering mainline module (shared with netd & system server)
// netutils_wrapper (for iptables xt_bpf) has access to programs
@@ -1412,6 +1414,13 @@
}
static int doLoad(char** argv, char * const envp[]) {
+ if (!isAtLeastS) {
+ ALOGE("Impossible - not reachable on Android <S.");
+ // for safety, we don't fail, this is a just-in-case workaround
+ // for any possible busted 'optimized' start everything vendor init hacks on R
+ return 0;
+ }
+
const bool runningAsRoot = !getuid(); // true iff U QPR3 or V+
const int first_api_level = GetIntProperty("ro.board.first_api_level", api_level);
@@ -1422,7 +1431,7 @@
const bool has_platform_netbpfload_rc = exists("/system/etc/init/netbpfload.rc");
// Version of Network BpfLoader depends on the Android OS version
- unsigned int bpfloader_ver = BPFLOADER_MAINLINE_VERSION; // [42u]
+ unsigned int bpfloader_ver = BPFLOADER_MAINLINE_S_VERSION; // [42u]
if (isAtLeastT) ++bpfloader_ver; // [43] BPFLOADER_MAINLINE_T_VERSION
if (isAtLeastU) ++bpfloader_ver; // [44] BPFLOADER_MAINLINE_U_VERSION
if (runningAsRoot) ++bpfloader_ver; // [45] BPFLOADER_MAINLINE_U_QPR3_VERSION
@@ -1446,14 +1455,9 @@
logTetheringApexVersion();
- if (!isAtLeastT) {
- ALOGE("Impossible - not reachable on Android <T.");
- return 1;
- }
-
// both S and T require kernel 4.9 (and eBpf support)
- if (isAtLeastT && !isAtLeastKernelVersion(4, 9, 0)) {
- ALOGE("Android T requires kernel 4.9.");
+ if (!isAtLeastKernelVersion(4, 9, 0)) {
+ ALOGE("Android S & T require kernel 4.9.");
return 1;
}
@@ -1622,18 +1626,22 @@
// which could otherwise fail with ENOENT during object pinning or renaming,
// due to ordering issues)
for (const auto& location : locations) {
+ if (location.t_plus && !isAtLeastT) continue;
if (createSysFsBpfSubDir(location.prefix)) return 1;
}
- // Note: there's no actual src dir for fs_bpf_loader .o's,
- // so it is not listed in 'locations[].prefix'.
- // This is because this is primarily meant for triggering genfscon rules,
- // and as such this will likely always be the case.
- // Thus we need to manually create the /sys/fs/bpf/loader subdirectory.
- if (createSysFsBpfSubDir("loader")) return 1;
+ if (isAtLeastT) {
+ // Note: there's no actual src dir for fs_bpf_loader .o's,
+ // so it is not listed in 'locations[].prefix'.
+ // This is because this is primarily meant for triggering genfscon rules,
+ // and as such this will likely always be the case.
+ // Thus we need to manually create the /sys/fs/bpf/loader subdirectory.
+ if (createSysFsBpfSubDir("loader")) return 1;
+ }
// Load all ELF objects, create programs and maps, and pin them
for (const auto& location : locations) {
+ if (location.t_plus && !isAtLeastT) continue;
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.");
@@ -1654,6 +1662,9 @@
return 1;
}
+ // on S we haven't created this subdir yet, but we need it for 'mainline_done' flag below
+ if (!isAtLeastT && createSysFsBpfSubDir("netd_shared")) return 1;
+
// leave a flag that we're done
if (createSysFsBpfSubDir("netd_shared/mainline_done")) return 1;
@@ -1688,7 +1699,12 @@
} // namespace android
int main(int argc, char** argv, char * const envp[]) {
- InitLogging(argv, &KernelLogger);
+ if (android::bpf::isAtLeastT) {
+ InitLogging(argv, &KernelLogger);
+ } else {
+ // S lacks the sepolicy to make non-root uid KernelLogger viable
+ InitLogging(argv);
+ }
if (argc == 2 && !strcmp(argv[1], "done")) {
// we're being re-exec'ed from platform bpfloader to 'finalize' things
diff --git a/bpf/loader/netbpfload.31rc b/bpf/loader/netbpfload.31rc
new file mode 100644
index 0000000..bca7dc8
--- /dev/null
+++ b/bpf/loader/netbpfload.31rc
@@ -0,0 +1,13 @@
+# This file takes effect only on S and Sv2
+# (Note: it does take effect on R as well, but isn't actually used)
+#
+# The service is started from netd's dnsresolver call into ADnsHelper_init()
+# on initial (boot time) startup of netd.
+
+service mdnsd_netbpfload /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
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ reboot_on_failure reboot,netbpfload-failed
diff --git a/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index e3e508b..d41aa81 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -341,8 +341,8 @@
if (chargeUid == AID_CLAT) return -EPERM;
// The socket destroy listener only monitors on the group {INET_TCP, INET_UDP, INET6_TCP,
- // INET6_UDP}. Tagging listener unsupported socket causes that the tag can't be removed from
- // tag map automatically. Eventually, the tag map may run out of space because of dead tag
+ // INET6_UDP}. Tagging listener unsupported sockets (on <5.10) means the tag cannot be
+ // removed from tag map automatically. Eventually, it may run out of space due to dead tag
// entries. Note that although tagSocket() of net client has already denied the family which
// is neither AF_INET nor AF_INET6, the family validation is still added here just in case.
// See tagSocket in system/netd/client/NetdClient.cpp and
@@ -360,15 +360,19 @@
return -EAFNOSUPPORT;
}
- int socketProto;
- socklen_t protoLen = sizeof(socketProto);
- if (getsockopt(sockFd, SOL_SOCKET, SO_PROTOCOL, &socketProto, &protoLen)) {
- ALOGE("Failed to getsockopt SO_PROTOCOL: %s, fd: %d", strerror(errno), sockFd);
- return -errno;
- }
- if (socketProto != IPPROTO_UDP && socketProto != IPPROTO_TCP) {
- ALOGV("Unsupported protocol: %d", socketProto);
- return -EPROTONOSUPPORT;
+ // On 5.10+ the BPF_CGROUP_INET_SOCK_RELEASE hook takes care of cookie tag map cleanup
+ // during socket destruction. As such the socket destroy listener is superfluous.
+ if (!isAtLeastKernelVersion(5, 10, 0)) {
+ int socketProto;
+ socklen_t protoLen = sizeof(socketProto);
+ if (getsockopt(sockFd, SOL_SOCKET, SO_PROTOCOL, &socketProto, &protoLen)) {
+ ALOGE("Failed to getsockopt SO_PROTOCOL: %s, fd: %d", strerror(errno), sockFd);
+ return -errno;
+ }
+ if (socketProto != IPPROTO_UDP && socketProto != IPPROTO_TCP) {
+ ALOGV("Unsupported protocol: %d", socketProto);
+ return -EPROTONOSUPPORT;
+ }
}
uint64_t sock_cookie = getSocketCookie(sockFd);
diff --git a/bpf/netd/BpfHandlerTest.cpp b/bpf/netd/BpfHandlerTest.cpp
index b38fa16..4002b4c 100644
--- a/bpf/netd/BpfHandlerTest.cpp
+++ b/bpf/netd/BpfHandlerTest.cpp
@@ -191,7 +191,11 @@
int rawSocket = socket(AF_INET, SOCK_RAW | SOCK_CLOEXEC, IPPROTO_RAW);
EXPECT_LE(0, rawSocket);
EXPECT_NE(NONEXISTENT_COOKIE, getSocketCookie(rawSocket));
- EXPECT_EQ(-EPROTONOSUPPORT, mBh.tagSocket(rawSocket, TEST_TAG, TEST_UID, TEST_UID));
+ if (isAtLeastKernelVersion(5, 10, 0)) {
+ EXPECT_EQ(0, mBh.tagSocket(rawSocket, TEST_TAG, TEST_UID, TEST_UID));
+ } else {
+ EXPECT_EQ(-EPROTONOSUPPORT, mBh.tagSocket(rawSocket, TEST_TAG, TEST_UID, TEST_UID));
+ }
}
TEST_F(BpfHandlerTest, TestTagSocketWithoutPermission) {
diff --git a/bpf/progs/Android.bp b/bpf/progs/Android.bp
index 20d194c..2bfe613 100644
--- a/bpf/progs/Android.bp
+++ b/bpf/progs/Android.bp
@@ -69,32 +69,16 @@
sub_dir: "net_shared",
}
-// Ships to Android S, the bpfloader of which fails to parse BTF enabled .o's.
bpf {
name: "offload.o",
srcs: ["offload.c"],
- btf: false,
+ sub_dir: "tethering",
}
-// This version ships to Android T+ which uses mainline netbpfload.
-bpf {
- name: "offload@mainline.o",
- srcs: ["offload@mainline.c"],
- cflags: ["-DMAINLINE"],
-}
-
-// Ships to Android S, the bpfloader of which fails to parse BTF enabled .o's.
bpf {
name: "test.o",
srcs: ["test.c"],
- btf: false,
-}
-
-// This version ships to Android T+ which uses mainline netbpfload.
-bpf {
- name: "test@mainline.o",
- srcs: ["test@mainline.c"],
- cflags: ["-DMAINLINE"],
+ sub_dir: "tethering",
}
bpf {
diff --git a/bpf/progs/offload.c b/bpf/progs/offload.c
index 0f23844..b34fe6f 100644
--- a/bpf/progs/offload.c
+++ b/bpf/progs/offload.c
@@ -14,16 +14,8 @@
* limitations under the License.
*/
-#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_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_T_VERSION
-#endif /* MAINLINE */
+// The resulting .o needs to load on Android S+
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_S_VERSION
#include "bpf_net_helpers.h"
#include "offload.h"
diff --git a/bpf/progs/test.c b/bpf/progs/test.c
index 8585118..4dba6b9 100644
--- a/bpf/progs/test.c
+++ b/bpf/progs/test.c
@@ -14,16 +14,8 @@
* limitations under the License.
*/
-#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_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_T_VERSION
-#endif /* MAINLINE */
+// The resulting .o needs to load on Android S+
+#define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_S_VERSION
// This is non production code, only used for testing
// Needed because the bitmap array definition is non-kosher for pre-T OS devices.
diff --git a/bpf/tests/mts/bpf_existence_test.cpp b/bpf/tests/mts/bpf_existence_test.cpp
index 75fb8e9..4d5f9b5 100644
--- a/bpf/tests/mts/bpf_existence_test.cpp
+++ b/bpf/tests/mts/bpf_existence_test.cpp
@@ -196,7 +196,12 @@
// S requires Linux Kernel 4.9+ and thus requires eBPF support.
if (isAtLeastS) ASSERT_TRUE(isAtLeastKernelVersion(4, 9, 0));
- DO_EXPECT(isAtLeastS, MAINLINE_FOR_S_PLUS);
+
+ // on S without a new enough DnsResolver apex, NetBpfLoad doesn't get triggered,
+ // and thus no mainline programs get loaded.
+ bool mainlineBpfCapableResolve = !access("/apex/com.android.resolv/NetBpfLoad-S.flag", F_OK);
+ bool mainlineNetBpfLoad = isAtLeastT || mainlineBpfCapableResolve;
+ DO_EXPECT(isAtLeastS && mainlineNetBpfLoad, MAINLINE_FOR_S_PLUS);
// Nothing added or removed in SCv2.
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index 317854b..2261c69 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -100,9 +100,9 @@
public static final long ENABLE_MATCH_LOCAL_NETWORK = 319212206L;
/**
- * On Android {@link android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM} or higher releases,
- * network access from apps targeting Android 36 or higher that do not have the
- * {@link android.Manifest.permission#INTERNET} permission is considered blocked.
+ * On Android versions starting from 37, network access from apps targeting
+ * Android 37 or higher, that do not have the {@link android.Manifest.permission#INTERNET}
+ * permission, is considered blocked.
* This results in API behaviors change for apps without
* {@link android.Manifest.permission#INTERNET} permission.
* {@link android.net.NetworkInfo} returned from {@link android.net.ConnectivityManager} APIs
@@ -115,10 +115,12 @@
* network access from apps without {@link android.Manifest.permission#INTERNET} permission is
* considered not blocked even though apps cannot access any networks.
*
+ * TODO: b/400903101 - Update the target SDK version once it's finalized.
+ *
* @hide
*/
@ChangeId
- @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @EnabledAfter(targetSdkVersion = 36)
public static final long NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION = 333340911L;
/**
diff --git a/networksecurity/service/Android.bp b/networksecurity/service/Android.bp
index d7aacdb..3c964e5 100644
--- a/networksecurity/service/Android.bp
+++ b/networksecurity/service/Android.bp
@@ -32,6 +32,7 @@
"framework-connectivity-pre-jarjar",
"service-connectivity-pre-jarjar",
"framework-statsd.stubs.module_lib",
+ "ServiceConnectivityResources",
],
static_libs: [
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
index e6f1379..f1b9a4f 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
@@ -38,6 +38,7 @@
private final DataStore mDataStore;
private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
private final CompatibilityVersion mCompatVersion;
+ private final SignatureVerifier mSignatureVerifier;
private final AlarmManager mAlarmManager;
private final PendingIntent mPendingIntent;
@@ -49,11 +50,13 @@
Context context,
DataStore dataStore,
CertificateTransparencyDownloader certificateTransparencyDownloader,
- CompatibilityVersion compatVersion) {
+ CompatibilityVersion compatVersion,
+ SignatureVerifier signatureVerifier) {
mContext = context;
mDataStore = dataStore;
mCertificateTransparencyDownloader = certificateTransparencyDownloader;
mCompatVersion = compatVersion;
+ mSignatureVerifier = signatureVerifier;
mAlarmManager = context.getSystemService(AlarmManager.class);
mPendingIntent =
@@ -127,6 +130,7 @@
private void startDependencies() {
mDataStore.load();
mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
+ mSignatureVerifier.loadAllowedKeys();
mContext.registerReceiver(
mCertificateTransparencyDownloader,
new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
@@ -139,6 +143,7 @@
private void stopDependencies() {
mContext.unregisterReceiver(mCertificateTransparencyDownloader);
+ mSignatureVerifier.clearAllowedKeys();
mCertificateTransparencyDownloader.clearCompatibilityVersions();
mDataStore.delete();
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
index a71ff7c..2e910b2 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -52,6 +52,7 @@
public CertificateTransparencyService(Context context) {
DataStore dataStore = new DataStore(Config.PREFERENCES_FILE);
+ SignatureVerifier signatureVerifier = new SignatureVerifier(context);
mCertificateTransparencyJob =
new CertificateTransparencyJob(
context,
@@ -60,13 +61,14 @@
context,
dataStore,
new DownloadHelper(context),
- new SignatureVerifier(context),
+ signatureVerifier,
new CertificateTransparencyLoggerImpl(dataStore)),
new CompatibilityVersion(
Config.COMPATIBILITY_VERSION,
Config.URL_SIGNATURE,
Config.URL_LOG_LIST,
- Config.CT_ROOT_DIRECTORY_PATH));
+ Config.CT_ROOT_DIRECTORY_PATH),
+ signatureVerifier);
}
/**
diff --git a/networksecurity/service/src/com/android/server/net/ct/PemReader.java b/networksecurity/service/src/com/android/server/net/ct/PemReader.java
new file mode 100644
index 0000000..56b3973
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/PemReader.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net.ct;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.spec.KeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collection;
+
+/** Utility class to read keys in PEM format. */
+class PemReader {
+
+ private static final String BEGIN = "-----BEGIN";
+ private static final String END = "-----END";
+
+ /**
+ * Parse the provided input stream and return the list of keys from the stream.
+ *
+ * @param input the input stream
+ * @return the keys
+ */
+ public static Collection<PublicKey> readKeysFrom(InputStream input)
+ throws IOException, GeneralSecurityException {
+ KeyFactory instance = KeyFactory.getInstance("RSA");
+ Collection<PublicKey> keys = new ArrayList<>();
+
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(input))) {
+ String line = reader.readLine();
+ while (line != null) {
+ if (line.startsWith(BEGIN)) {
+ keys.add(instance.generatePublic(readNextKey(reader)));
+ } else {
+ throw new IOException("Unexpected line in the reader: " + line);
+ }
+ line = reader.readLine();
+ }
+ } catch (IllegalArgumentException e) {
+ throw new GeneralSecurityException("Invalid public key base64 encoding", e);
+ }
+
+ return keys;
+ }
+
+ private static KeySpec readNextKey(BufferedReader reader) throws IOException {
+ StringBuilder publicKeyBuilder = new StringBuilder();
+
+ String line = reader.readLine();
+ while (line != null) {
+ if (line.startsWith(END)) {
+ return new X509EncodedKeySpec(
+ Base64.getDecoder().decode(publicKeyBuilder.toString()));
+ } else {
+ publicKeyBuilder.append(line);
+ }
+ line = reader.readLine();
+ }
+
+ throw new IOException("Unexpected end of the reader");
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
index 6040ef6..87a4973 100644
--- a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
+++ b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
@@ -30,6 +30,9 @@
import androidx.annotation.VisibleForTesting;
+import com.android.connectivity.resources.R;
+import com.android.server.connectivity.ConnectivityResources;
+
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
@@ -39,21 +42,39 @@
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
+import java.util.HashSet;
import java.util.Optional;
+import java.util.Set;
/** Verifier of the log list signature. */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
public class SignatureVerifier {
- private final Context mContext;
private static final String TAG = "SignatureVerifier";
+ private final Context mContext;
+
@NonNull private Optional<PublicKey> mPublicKey = Optional.empty();
+ private final Set<PublicKey> mAllowedKeys = new HashSet<>();
+
public SignatureVerifier(Context context) {
mContext = context;
}
+ void loadAllowedKeys() {
+ try (InputStream input =
+ new ConnectivityResources(mContext).get().openRawResource(R.raw.ct_public_keys)) {
+ mAllowedKeys.addAll(PemReader.readKeysFrom(input));
+ } catch (GeneralSecurityException | IOException e) {
+ Log.e(TAG, "Error loading public keys", e);
+ }
+ }
+
+ void clearAllowedKeys() {
+ mAllowedKeys.clear();
+ }
+
@VisibleForTesting
Optional<PublicKey> getPublicKey() {
return mPublicKey;
@@ -82,7 +103,11 @@
}
@VisibleForTesting
- void setPublicKey(PublicKey publicKey) {
+ void setPublicKey(PublicKey publicKey) throws GeneralSecurityException {
+ if (!mAllowedKeys.contains(publicKey)) {
+ // TODO(b/400704086): add logging for this failure.
+ throw new GeneralSecurityException("Public key not in allowlist");
+ }
mPublicKey = Optional.of(publicKey);
}
@@ -105,21 +130,18 @@
byte[] signatureBytes = signatureStream.readAllBytes();
statusBuilder.setSignature(new String(signatureBytes));
- try {
- byte[] decodedSigBytes = Base64.getDecoder().decode(signatureBytes);
- if (!verifier.verify(decodedSigBytes)) {
- // Leave the UpdateState as UNKNOWN_STATE if successful as there are other
- // potential failures past the signature verification step
- statusBuilder.setState(SIGNATURE_VERIFICATION_FAILED);
- }
- } catch (IllegalArgumentException e) {
- Log.w(TAG, "Invalid signature base64 encoding", e);
- statusBuilder.setState(SIGNATURE_INVALID);
- return statusBuilder.build();
+ if (!verifier.verify(Base64.getDecoder().decode(signatureBytes))) {
+ // Leave the UpdateState as UNKNOWN_STATE if successful as there are other
+ // potential failures past the signature verification step
+ statusBuilder.setState(SIGNATURE_VERIFICATION_FAILED);
}
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Invalid signature base64 encoding", e);
+ statusBuilder.setState(SIGNATURE_INVALID);
+ return statusBuilder.build();
} catch (InvalidKeyException e) {
- Log.e(TAG, "Signature invalid for log list verification", e);
+ Log.e(TAG, "Key invalid for log list verification", e);
statusBuilder.setState(SIGNATURE_INVALID);
return statusBuilder.build();
} catch (IOException | GeneralSecurityException e) {
@@ -135,4 +157,9 @@
return statusBuilder.build();
}
+
+ @VisibleForTesting
+ boolean addAllowedKey(PublicKey publicKey) {
+ return mAllowedKeys.add(publicKey);
+ }
}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index 2af0122..22dc6ab 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -109,6 +109,7 @@
mContext.getFilesDir());
prepareDownloadManager();
+ mSignatureVerifier.addAllowedKey(mPublicKey);
mDataStore.load();
mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
}
@@ -165,6 +166,22 @@
@Test
public void
+ testDownloader_publicKeyDownloadSuccess_publicKeyNotAllowed_doNotStartMetadataDownload()
+ throws Exception {
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
+ PublicKey notAllowed = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic();
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makePublicKeyDownloadCompleteIntent(writePublicKeyToFile(notAllowed)));
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ }
+
+ @Test
+ public void
testDownloader_publicKeyDownloadSuccess_updatePublicKeyFail_doNotStartMetadataDownload()
throws Exception {
mCertificateTransparencyDownloader.startPublicKeyDownload();
@@ -197,8 +214,7 @@
}
@Test
- public void testDownloader_publicKeyDownloadFail_logsFailure()
- throws Exception {
+ public void testDownloader_publicKeyDownloadFail_logsFailure() throws Exception {
mCertificateTransparencyDownloader.startPublicKeyDownload();
mCertificateTransparencyDownloader.onReceive(
@@ -243,8 +259,7 @@
}
@Test
- public void testDownloader_metadataDownloadFail_logsFailure()
- throws Exception {
+ public void testDownloader_metadataDownloadFail_logsFailure() throws Exception {
mCertificateTransparencyDownloader.startMetadataDownload();
mCertificateTransparencyDownloader.onReceive(
@@ -294,8 +309,7 @@
}
@Test
- public void testDownloader_contentDownloadFail_logsFailure()
- throws Exception {
+ public void testDownloader_contentDownloadFail_logsFailure() throws Exception {
mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
mCertificateTransparencyDownloader.onReceive(
@@ -329,9 +343,8 @@
}
@Test
- public void
- testDownloader_contentDownloadSuccess_noPublicKeyFound_logsSingleFailure()
- throws Exception {
+ public void testDownloader_contentDownloadSuccess_noPublicKeyFound_logsSingleFailure()
+ throws Exception {
File logListFile = makeLogListFile("456");
File metadataFile = sign(logListFile);
mSignatureVerifier.setPublicKey(mPublicKey);
@@ -351,16 +364,17 @@
}
@Test
- public void
- testDownloader_contentDownloadSuccess_wrongSignatureAlgo_logsSingleFailure()
- throws Exception {
+ public void testDownloader_contentDownloadSuccess_wrongSignatureAlgo_logsSingleFailure()
+ throws Exception {
// Arrange
File logListFile = makeLogListFile("456");
File metadataFile = sign(logListFile);
// Set the key to be deliberately wrong by using diff algorithm
- KeyPairGenerator instance = KeyPairGenerator.getInstance("EC");
- mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+ PublicKey wrongAlgorithmKey =
+ KeyPairGenerator.getInstance("EC").generateKeyPair().getPublic();
+ mSignatureVerifier.addAllowedKey(wrongAlgorithmKey);
+ mSignatureVerifier.setPublicKey(wrongAlgorithmKey);
// Act
mCertificateTransparencyDownloader.startMetadataDownload();
@@ -377,16 +391,15 @@
}
@Test
- public void
- testDownloader_contentDownloadSuccess_signatureNotVerified_logsSingleFailure()
- throws Exception {
+ public void testDownloader_contentDownloadSuccess_signatureNotVerified_logsSingleFailure()
+ throws Exception {
// Arrange
File logListFile = makeLogListFile("456");
- File metadataFile = sign(logListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
- // Set the key to be deliberately wrong by using diff key pair
+ // Sign the list with a disallowed key pair
KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
- mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+ File metadataFile = sign(logListFile, instance.generateKeyPair().getPrivate());
// Act
mCertificateTransparencyDownloader.startMetadataDownload();
@@ -405,9 +418,7 @@
}
@Test
- public void
- testDownloader_contentDownloadSuccess_installFail_logsFailure()
- throws Exception {
+ public void testDownloader_contentDownloadSuccess_installFail_logsFailure() throws Exception {
File invalidLogListFile = writeToFile("not_a_json_log_list".getBytes());
File metadataFile = sign(invalidLogListFile);
mSignatureVerifier.setPublicKey(mPublicKey);
@@ -615,9 +626,14 @@
}
private File sign(File file) throws IOException, GeneralSecurityException {
+ return sign(file, mPrivateKey);
+ }
+
+ private File sign(File file, PrivateKey privateKey)
+ throws IOException, GeneralSecurityException {
File signatureFile = File.createTempFile("log_list-metadata", "sig");
Signature signer = Signature.getInstance("SHA256withRSA");
- signer.initSign(mPrivateKey);
+ signer.initSign(privateKey);
try (InputStream fileStream = new FileInputStream(file);
OutputStream outputStream = new FileOutputStream(signatureFile)) {
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/PemReaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/PemReaderTest.java
new file mode 100644
index 0000000..08629db
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/PemReaderTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+import java.util.Base64;
+
+/** Tests for the {@link PemReader}. */
+@RunWith(JUnit4.class)
+public class PemReaderTest {
+
+ @Test
+ public void testReadKeys_singleKey() throws GeneralSecurityException, IOException {
+ PublicKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic();
+
+ assertThat(PemReader.readKeysFrom(toInputStream(key))).containsExactly(key);
+ }
+
+ @Test
+ public void testReadKeys_multipleKeys() throws GeneralSecurityException, IOException {
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
+ PublicKey key1 = instance.generateKeyPair().getPublic();
+ PublicKey key2 = instance.generateKeyPair().getPublic();
+
+ assertThat(PemReader.readKeysFrom(toInputStream(key1, key2))).containsExactly(key1, key2);
+ }
+
+ @Test
+ public void testReadKeys_notSupportedKeyType() throws GeneralSecurityException {
+ PublicKey key = KeyPairGenerator.getInstance("EC").generateKeyPair().getPublic();
+
+ assertThrows(
+ GeneralSecurityException.class, () -> PemReader.readKeysFrom(toInputStream(key)));
+ }
+
+ @Test
+ public void testReadKeys_notBase64EncodedKey() throws GeneralSecurityException {
+ InputStream inputStream =
+ new ByteArrayInputStream(
+ (""
+ + "-----BEGIN PUBLIC KEY-----\n"
+ + KeyPairGenerator.getInstance("RSA")
+ .generateKeyPair()
+ .getPublic()
+ .toString()
+ + "\n-----END PUBLIC KEY-----\n")
+ .getBytes());
+
+ assertThrows(GeneralSecurityException.class, () -> PemReader.readKeysFrom(inputStream));
+ }
+
+ @Test
+ public void testReadKeys_noPemBegin() throws GeneralSecurityException {
+ PublicKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic();
+ String base64Key = Base64.getEncoder().encodeToString(key.getEncoded());
+ String pemNoBegin = base64Key + "\n-----END PUBLIC KEY-----\n";
+
+ assertThrows(
+ IOException.class,
+ () -> PemReader.readKeysFrom(new ByteArrayInputStream(pemNoBegin.getBytes())));
+ }
+
+ @Test
+ public void testReadKeys_noPemEnd() throws GeneralSecurityException {
+ PublicKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic();
+ String base64Key = Base64.getEncoder().encodeToString(key.getEncoded());
+ String pemNoEnd = "-----BEGIN PUBLIC KEY-----\n" + base64Key;
+
+ assertThrows(
+ IOException.class,
+ () -> PemReader.readKeysFrom(new ByteArrayInputStream(pemNoEnd.getBytes())));
+ }
+
+ private InputStream toInputStream(PublicKey... keys) {
+ StringBuilder builder = new StringBuilder();
+
+ for (PublicKey key : keys) {
+ builder.append("-----BEGIN PUBLIC KEY-----\n")
+ .append(Base64.getEncoder().encodeToString(key.getEncoded()))
+ .append("\n-----END PUBLIC KEY-----\n");
+ }
+
+ return new ByteArrayInputStream(builder.toString().getBytes());
+ }
+}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index d2e2a80..ab38c7a 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -29,6 +29,7 @@
name: "service-connectivity-tiramisu-sources",
srcs: [
"src/**/*.java",
+ ":vcn-location-sources",
],
visibility: ["//visibility:private"],
}
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index 5d23fdc..5ef1aef 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -30,6 +30,9 @@
import com.android.server.nearby.NearbyService;
import com.android.server.net.ct.CertificateTransparencyService;
import com.android.server.thread.ThreadNetworkService;
+import com.android.server.vcn.VcnLocation;
+
+import java.lang.reflect.Constructor;
/**
* Connectivity service initializer for core networking. This is called by system server to create
@@ -37,6 +40,9 @@
*/
public final class ConnectivityServiceInitializer extends SystemService {
private static final String TAG = ConnectivityServiceInitializer.class.getSimpleName();
+ private static final String CONNECTIVITY_SERVICE_INITIALIZER_B_CLASS =
+ "com.android.server.ConnectivityServiceInitializerB";
+
private final ConnectivityNativeService mConnectivityNative;
private final ConnectivityService mConnectivity;
private final IpSecService mIpSecService;
@@ -45,6 +51,7 @@
private final EthernetServiceImpl mEthernetServiceImpl;
private final ThreadNetworkService mThreadNetworkService;
private final CertificateTransparencyService mCertificateTransparencyService;
+ private final SystemService mConnectivityServiceInitializerB;
public ConnectivityServiceInitializer(Context context) {
super(context);
@@ -58,6 +65,7 @@
mNearbyService = createNearbyService(context);
mThreadNetworkService = createThreadNetworkService(context);
mCertificateTransparencyService = createCertificateTransparencyService(context);
+ mConnectivityServiceInitializerB = createConnectivityServiceInitializerB(context);
}
@Override
@@ -99,6 +107,11 @@
publishBinderService(ThreadNetworkManager.SERVICE_NAME, mThreadNetworkService,
/* allowIsolated= */ false);
}
+
+ if (mConnectivityServiceInitializerB != null) {
+ Log.i(TAG, "ConnectivityServiceInitializerB#onStart");
+ mConnectivityServiceInitializerB.onStart();
+ }
}
@Override
@@ -118,6 +131,10 @@
if (SdkLevel.isAtLeastV() && mCertificateTransparencyService != null) {
mCertificateTransparencyService.onBootPhase(phase);
}
+
+ if (mConnectivityServiceInitializerB != null) {
+ mConnectivityServiceInitializerB.onBootPhase(phase);
+ }
}
/**
@@ -202,4 +219,28 @@
? new CertificateTransparencyService(context)
: null;
}
+
+ // TODO: b/374174952 After VCN code is moved to the Connectivity folder, merge
+ // ConnectivityServiceInitializerB into ConnectivityServiceInitializer and directly create and
+ // register VcnManagementService in ConnectivityServiceInitializer
+ /** Return ConnectivityServiceInitializerB instance if enable, otherwise null. */
+ @Nullable
+ private SystemService createConnectivityServiceInitializerB(Context context) {
+ if (!VcnLocation.IS_VCN_IN_MAINLINE || !SdkLevel.isAtLeastB()) {
+ return null;
+ }
+
+ try {
+ final Class<?> connectivityServiceInitializerBClass =
+ Class.forName(CONNECTIVITY_SERVICE_INITIALIZER_B_CLASS);
+ final Constructor constructor =
+ connectivityServiceInitializerBClass.getConstructor(Context.class);
+
+ return (SystemService) constructor.newInstance(context);
+ } catch (Exception e) {
+ Log.e(TAG, "Fail to load ConnectivityServiceInitializerB " + e);
+ }
+
+ return null;
+ }
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
index c7d6850..4b9429b 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
@@ -430,19 +430,32 @@
* @param dumpsysCmd The dumpsys command to run (for example "connectivity").
* @param exceptionContext An exception to write a stacktrace to the dump for context.
*/
- fun collectDumpsys(dumpsysCmd: String, exceptionContext: Throwable? = null) {
- Log.i(TAG, "Collecting dumpsys $dumpsysCmd for test artifacts")
+ fun collectDumpsys(dumpsysCmd: String, exceptionContext: Throwable? = null) =
+ collectCommandOutput("dumpsys $dumpsysCmd", exceptionContext = exceptionContext)
+
+ /**
+ * Add the output of a command to the test data dump.
+ *
+ * <p>The output will be collected immediately, and exported to a test artifact file when the
+ * test ends.
+ * @param cmd The command to run. Stdout of the command will be collected.
+ * @param shell The shell to run the command in.
+ * @param exceptionContext An exception to write a stacktrace to the dump for context.
+ */
+ fun collectCommandOutput(
+ cmd: String,
+ shell: String = "sh",
+ exceptionContext: Throwable? = null
+ ) {
+ Log.i(TAG, "Collecting '$cmd' for test artifacts")
PrintWriter(buffer).let {
- it.println("--- Dumpsys $dumpsysCmd at ${ZonedDateTime.now()} ---")
+ it.println("--- $cmd at ${ZonedDateTime.now()} ---")
maybeWriteExceptionContext(it, exceptionContext)
it.flush()
}
- ParcelFileDescriptor.AutoCloseInputStream(
- InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand(
- "dumpsys $dumpsysCmd"
- )
- ).use {
- it.copyTo(buffer)
+
+ runCommandInShell(cmd, shell) { stdout, _ ->
+ stdout.copyTo(buffer)
}
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ShellUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ShellUtil.kt
new file mode 100644
index 0000000..fadc2ab
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ShellUtil.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("ShellUtil")
+
+package com.android.testutils
+
+import android.app.UiAutomation
+import android.os.ParcelFileDescriptor.AutoCloseInputStream
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream
+import androidx.test.platform.app.InstrumentationRegistry
+import java.io.InputStream
+
+/**
+ * Run a command in a shell.
+ *
+ * Compared to [UiAutomation.executeShellCommand], this allows running commands with pipes and
+ * redirections. [UiAutomation.executeShellCommand] splits the command on spaces regardless of
+ * quotes, so it is not able to run commands like `sh -c "echo 123 > some_file"`.
+ *
+ * @param cmd Shell command to run.
+ * @param shell Command used to run the shell.
+ * @param outputProcessor Function taking stdout, stderr as argument. The streams will be closed
+ * when this function returns.
+ * @return Result of [outputProcessor].
+ */
+fun <T> runCommandInShell(
+ cmd: String,
+ shell: String = "sh",
+ outputProcessor: (InputStream, InputStream) -> T,
+): T {
+ val (stdout, stdin, stderr) = InstrumentationRegistry.getInstrumentation().uiAutomation
+ .executeShellCommandRwe(shell)
+ AutoCloseOutputStream(stdin).bufferedWriter().use { it.write(cmd) }
+ AutoCloseInputStream(stdout).use { outStream ->
+ AutoCloseInputStream(stderr).use { errStream ->
+ return outputProcessor(outStream, errStream)
+ }
+ }
+}
+
+/**
+ * Run a command in a shell.
+ *
+ * Overload of [runCommandInShell] that reads and returns stdout as String.
+ */
+fun runCommandInShell(
+ cmd: String,
+ shell: String = "sh",
+) = runCommandInShell(cmd, shell) { stdout, _ ->
+ stdout.reader().use { it.readText() }
+}
+
+/**
+ * Run a command in a root shell.
+ *
+ * This is generally only usable on devices on which [DeviceInfoUtils.isDebuggable] is true.
+ * @see runCommandInShell
+ */
+fun runCommandInRootShell(
+ cmd: String
+) = runCommandInShell(cmd, shell = "su root sh")
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 3430196..d55df6f 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
@@ -1843,11 +1843,11 @@
final DetailedBlockedStatusCallback remoteUidCallback = new DetailedBlockedStatusCallback();
// Create a TUN interface
- final FileDescriptor tunFd = runWithShellPermissionIdentity(() -> {
+ final ParcelFileDescriptor tunFd = runWithShellPermissionIdentity(() -> {
final TestNetworkManager tnm = mTestContext.getSystemService(TestNetworkManager.class);
final TestNetworkInterface iface = tnm.createTunInterface(List.of(
TEST_IP4_DST_ADDR, TEST_IP6_DST_ADDR));
- return iface.getFileDescriptor().getFileDescriptor();
+ return iface.getFileDescriptor();
}, MANAGE_TEST_NETWORKS);
// Create a remote UDP socket
@@ -1861,7 +1861,7 @@
remoteUidCallback.expectAvailableCallbacksWithBlockedReasonNone(network);
// The remote UDP socket can receive packets coming from the TUN interface
- checkBlockIncomingPacket(tunFd, remoteUdpFd, EXPECT_PASS);
+ checkBlockIncomingPacket(tunFd.getFileDescriptor(), remoteUdpFd, EXPECT_PASS);
// Lockdown uid that has the remote UDP socket
runWithShellPermissionIdentity(() -> {
@@ -1877,7 +1877,7 @@
if (SdkLevel.isAtLeastT()) {
// On T and above, lockdown rule drop packets not coming from lo regardless of the
// VPN connectivity.
- checkBlockIncomingPacket(tunFd, remoteUdpFd, EXPECT_BLOCK);
+ checkBlockIncomingPacket(tunFd.getFileDescriptor(), remoteUdpFd, EXPECT_BLOCK);
}
// Start the VPN that has default routes. This VPN should have interface filtering rule
@@ -1889,9 +1889,9 @@
null /* proxyInfo */, null /* underlyingNetworks */,
false /* isAlwaysMetered */);
- checkBlockIncomingPacket(tunFd, remoteUdpFd, EXPECT_BLOCK);
+ checkBlockIncomingPacket(tunFd.getFileDescriptor(), remoteUdpFd, EXPECT_BLOCK);
}, /* cleanup */ () -> {
- Os.close(tunFd);
+ tunFd.close();
}, /* cleanup */ () -> {
Os.close(remoteUdpFd);
}, /* cleanup */ () -> {
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
index 75b2814..27cba3a 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
@@ -160,11 +160,15 @@
@Override
public void onStopTetheringSucceeded() {
mHistory.add(new CallbackValue.OnStopTetheringSucceeded());
+ // Call the parent method so that the coverage linter sees it: http://b/385014495
+ TetheringManager.StopTetheringCallback.super.onStopTetheringSucceeded();
}
@Override
public void onStopTetheringFailed(final int error) {
mHistory.add(new CallbackValue.OnStopTetheringFailed(error));
+ // Call the parent method so that the coverage linter sees it: http://b/385014495
+ TetheringManager.StopTetheringCallback.super.onStopTetheringFailed(error);
}
/**
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
index 0a8f108..976dfa9 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
@@ -208,7 +208,10 @@
@Test
fun testServiceExpiredAndSendCallbacks() {
val serviceCache = MdnsServiceCache(
- thread.looper, makeFlags(isExpiredServicesRemovalEnabled = true), clock)
+ thread.looper,
+ makeFlags(isExpiredServicesRemovalEnabled = true),
+ clock
+ )
// Register service expired callbacks
val callback1 = ExpiredRecord()
val callback2 = ExpiredRecord()
@@ -218,12 +221,21 @@
doReturn(TEST_ELAPSED_REALTIME_MS).`when`(clock).elapsedRealtime()
// Add multiple services with different ttl time.
- addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1,
- DEFAULT_TTL_TIME_MS))
- addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1,
- DEFAULT_TTL_TIME_MS + 20L))
- addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_3, SERVICE_TYPE_2,
- DEFAULT_TTL_TIME_MS + 10L))
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(
+ SERVICE_NAME_1,
+ SERVICE_TYPE_1,
+ DEFAULT_TTL_TIME_MS
+ ))
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(
+ SERVICE_NAME_2,
+ SERVICE_TYPE_1,
+ DEFAULT_TTL_TIME_MS + 20L
+ ))
+ addOrUpdateService(serviceCache, cacheKey2, createResponse(
+ SERVICE_NAME_3,
+ SERVICE_TYPE_2,
+ DEFAULT_TTL_TIME_MS + 10L
+ ))
// Check the service expiration immediately. Should be no callback.
assertEquals(2, getServices(serviceCache, cacheKey1).size)
@@ -252,16 +264,25 @@
@Test
fun testRemoveExpiredServiceWhenGetting() {
val serviceCache = MdnsServiceCache(
- thread.looper, makeFlags(isExpiredServicesRemovalEnabled = true), clock)
+ thread.looper,
+ makeFlags(isExpiredServicesRemovalEnabled = true),
+ clock
+ )
doReturn(TEST_ELAPSED_REALTIME_MS).`when`(clock).elapsedRealtime()
- addOrUpdateService(serviceCache, cacheKey1,
- createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 1L /* ttlTime */))
+ addOrUpdateService(
+ serviceCache,
+ cacheKey1,
+ createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 1L /* ttlTime */)
+ )
doReturn(TEST_ELAPSED_REALTIME_MS + 2L).`when`(clock).elapsedRealtime()
assertNull(getService(serviceCache, SERVICE_NAME_1, cacheKey1))
- addOrUpdateService(serviceCache, cacheKey2,
- createResponse(SERVICE_NAME_2, SERVICE_TYPE_2, 3L /* ttlTime */))
+ addOrUpdateService(
+ serviceCache,
+ cacheKey2,
+ createResponse(SERVICE_NAME_2, SERVICE_TYPE_2, 3L /* ttlTime */)
+ )
doReturn(TEST_ELAPSED_REALTIME_MS + 4L).`when`(clock).elapsedRealtime()
assertEquals(0, getServices(serviceCache, cacheKey2).size)
}
@@ -334,8 +355,11 @@
): MdnsResponse {
val serviceName = "$serviceInstanceName.$serviceType".split(".").toTypedArray()
val response = MdnsResponse(
- 0 /* now */, "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
- socketKey.interfaceIndex, socketKey.network)
+ 0 /* now */,
+ "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
+ socketKey.interfaceIndex,
+ socketKey.network
+ )
// Set PTR record
val pointerRecord = MdnsPointerRecord(
@@ -343,7 +367,8 @@
TEST_ELAPSED_REALTIME_MS /* receiptTimeMillis */,
false /* cacheFlush */,
ttlTime /* ttlMillis */,
- serviceName)
+ serviceName
+ )
response.addPointerRecord(pointerRecord)
// Set SRV record.
@@ -355,7 +380,8 @@
0 /* servicePriority */,
0 /* serviceWeight */,
12345 /* port */,
- arrayOf("hostname"))
+ arrayOf("hostname")
+ )
response.serviceRecord = serviceRecord
return response
}
diff --git a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
index 6c2a9bb..f959ccf 100644
--- a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -113,8 +113,8 @@
@Before
public void setUp() throws Exception {
- mOtCtl.factoryReset();
mController.setEnabledAndWait(true);
+ mController.leaveAndWait();
mController.joinAndWait(DEFAULT_DATASET);
mNsdManager = mContext.getSystemService(NsdManager.class);
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index 7a5895f..2641a77 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -132,10 +132,6 @@
mOtCtl = new OtDaemonController();
mController.setEnabledAndWait(true);
mController.leaveAndWait();
-
- // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
- mOtCtl.factoryReset();
-
mFtd = new FullThreadDevice(10 /* nodeId */);
}
@@ -352,7 +348,6 @@
mOtCtl.executeCommand("netdata register");
mController.leaveAndWait();
- mOtCtl.factoryReset();
mController.joinAndWait(DEFAULT_DATASET);
LinkProperties lp = cm.getLinkProperties(getThreadNetwork(CALLBACK_TIMEOUT));
diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
index 2f0ab34..ac688dd 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
@@ -66,11 +66,7 @@
@Before
public void setUp() throws Exception {
- // TODO(b/366141754): The current implementation of "thread_network ot-ctl factoryreset"
- // results in timeout error.
- // A future fix will provide proper support for factoryreset, allowing us to replace the
- // legacy "ot-ctl".
- mOtCtl.factoryReset();
+ mController.leaveAndWait();
mFtd = new FullThreadDevice(10 /* nodeId */);
ensureThreadEnabled();
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
index 801e21e..f00c9cd 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -603,11 +603,12 @@
/** Enables Thread and joins the specified Thread network. */
@JvmStatic
fun enableThreadAndJoinNetwork(dataset: ActiveOperationalDataset) {
- // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
- OtDaemonController().factoryReset();
-
val context: Context = requireNotNull(ApplicationProvider.getApplicationContext());
val controller = requireNotNull(ThreadNetworkControllerWrapper.newInstance(context));
+
+ // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
+ controller.leaveAndWait();
+
controller.setEnabledAndWait(true);
controller.joinAndWait(dataset);
}