diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java index 3a7d070670..5a939219b2 100644 --- a/framework/src/android/net/ConnectivityManager.java +++ b/framework/src/android/net/ConnectivityManager.java @@ -3974,7 +3974,8 @@ public class ConnectivityManager { */ @RequiresPermission(anyOf = { NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, - android.Manifest.permission.NETWORK_FACTORY}) + android.Manifest.permission.NETWORK_FACTORY, + android.Manifest.permission.CREATE_APP_SPECIFIC_NETWORK}) public NetworkAndAgentRegistryParcelable registerNetworkAgent( @NonNull INetworkAgent na, @NonNull NetworkInfo ni, @NonNull LinkProperties lp, @NonNull NetworkCapabilities nc, @@ -3990,7 +3991,8 @@ public class ConnectivityManager { */ @RequiresPermission(anyOf = { NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, - android.Manifest.permission.NETWORK_FACTORY}) + android.Manifest.permission.NETWORK_FACTORY, + android.Manifest.permission.CREATE_APP_SPECIFIC_NETWORK}) public NetworkAndAgentRegistryParcelable registerNetworkAgent( @NonNull INetworkAgent na, @NonNull NetworkInfo ni, @NonNull LinkProperties lp, @NonNull NetworkCapabilities nc, diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index 35efabdaef..2e2fe1dbc1 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -1565,6 +1565,10 @@ public class ConnectivityService extends IConnectivityManager.Stub return SdkLevel.isAtLeastB(); } + public boolean isAtLeast25Q4() { + return Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.BAKLAVA_1; + } + /** Get SystemClock.elapsedRealtime() */ public long getElapsedRealtime() { return SystemClock.elapsedRealtime(); @@ -2312,7 +2316,7 @@ public class ConnectivityService extends IConnectivityManager.Stub new LinkProperties(), new NetworkCapabilities(), null /* localNetworkConfig */, new NetworkScore.Builder().setLegacyInt(0).build(), mContext, null, new NetworkAgentConfig(), this, null, null, 0, INVALID_UID, - mLingerDelayMs, mQosCallbackTracker, mDeps); + false /* isAppSpecificNetwork */, mLingerDelayMs, mQosCallbackTracker, mDeps); try { // DscpPolicyTracker cannot run on S because on S the tethering module can only load @@ -4099,14 +4103,28 @@ public class ConnectivityService extends IConnectivityManager.Stub Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS); } - private void enforceNetworkFactoryPermission() { + private boolean hasCreateAppSpecificNetworkPermission() { + // The CREATE_APP_SPECIFIC_NETWORK permission was introduced in 25Q4. + // It must not be granted on earlier platform versions, even if an app declares it. + return mDeps.isAtLeast25Q4() && hasAnyPermissionOf(mContext, + android.Manifest.permission.CREATE_APP_SPECIFIC_NETWORK); + } + + private boolean hasNetworkFactoryPermission() { // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface. - if (UserHandle.getAppId(getCallingUid()) == Process.BLUETOOTH_UID) return; - enforceAnyPermissionOf(mContext, + if (UserHandle.getAppId(getCallingUid()) == Process.BLUETOOTH_UID) return true; + return hasAnyPermissionOf(mContext, android.Manifest.permission.NETWORK_FACTORY, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); } + private void enforceNetworkFactoryPermission() { + if (!hasNetworkFactoryPermission()) { + throw new SecurityException("Requires one of the following permissions: " + + "NETWORK_FACTORY, MAINLINE_NETWORK_STACK"); + } + } + private void enforceNetworkFactoryOrSettingsPermission() { // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface. if (UserHandle.getAppId(getCallingUid()) == Process.BLUETOOTH_UID) return; @@ -9873,10 +9891,19 @@ public class ConnectivityService extends IConnectivityManager.Stub Objects.requireNonNull(networkCapabilities, "networkCapabilities must not be null"); Objects.requireNonNull(initialScore, "initialScore must not be null"); Objects.requireNonNull(networkAgentConfig, "networkAgentConfig must not be null"); + boolean isAppSpecificNetwork = false; if (networkCapabilities.hasTransport(TRANSPORT_TEST)) { enforceAnyPermissionOf(mContext, Manifest.permission.MANAGE_TEST_NETWORKS); + } else if (hasNetworkFactoryPermission()) { + // Factory permission is present. Do nothing and proceed. + } else if (hasCreateAppSpecificNetworkPermission()) { + // The app lacks the network factory permission but has + // CREATE_APP_SPECIFIC_NETWORK permission. + isAppSpecificNetwork = true; } else { - enforceNetworkFactoryPermission(); + throw new SecurityException("Requires one of the following permissions: " + + "NETWORK_FACTORY, MAINLINE_NETWORK_STACK" + + (mDeps.isAtLeast25Q4() ? ", CREATE_APP_SPECIFIC_NETWORK" : "")); } final boolean hasLocalCap = networkCapabilities.hasCapability(NET_CAPABILITY_LOCAL_NETWORK); @@ -9897,7 +9924,7 @@ public class ConnectivityService extends IConnectivityManager.Stub try { return registerNetworkAgentInternal(na, networkInfo, linkProperties, networkCapabilities, initialScore, networkAgentConfig, localNetworkConfig, - providerId, uid); + providerId, uid, isAppSpecificNetwork); } finally { Binder.restoreCallingIdentity(token); } @@ -9908,7 +9935,7 @@ public class ConnectivityService extends IConnectivityManager.Stub LinkProperties linkProperties, NetworkCapabilities networkCapabilities, NetworkScore currentScore, NetworkAgentConfig networkAgentConfig, @Nullable LocalNetworkConfig localNetworkConfig, int providerId, - int uid) { + int uid, boolean isAppSpecificNetwork) { // Make a copy of the passed NI, LP, NC as the caller may hold a reference to them // and mutate them at any time. @@ -9917,6 +9944,12 @@ public class ConnectivityService extends IConnectivityManager.Stub final LinkProperties lpCopy = new LinkProperties(linkProperties); // No need to copy |localNetworkConfiguration| as it is immutable. + if (isAppSpecificNetwork) { + // For app specific network, set the app's UID and make network restricted. + ncCopy.setSingleUid(uid); + ncCopy.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + } + // At this point the capabilities/properties are untrusted and unverified, e.g. checks that // the capabilities' access UIDs comply with security limitations. They will be sanitized // as the NAI registration finishes, in handleRegisterNetworkAgent(). This is @@ -9925,7 +9958,7 @@ public class ConnectivityService extends IConnectivityManager.Stub new Network(mNetIdManager.reserveNetId()), niCopy, lpCopy, ncCopy, localNetworkConfig, currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig), this, mNetd, mDnsResolver, providerId, - uid, mLingerDelayMs, mQosCallbackTracker, mDeps); + uid, isAppSpecificNetwork, mLingerDelayMs, mQosCallbackTracker, mDeps); final String extraInfo = niCopy.getExtraInfo(); final String name = TextUtils.isEmpty(extraInfo) diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java index 6fac75bf10..253ff9ed81 100644 --- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java +++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java @@ -437,6 +437,9 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable { // The UID of the remote entity that created this Network. public final int creatorUid; + // Indicates whether this network is specific to the app that registered the networkAgent. + private final boolean mIsAppSpecificNetwork; + // Network agent portal info of the network, if any. This information is provided from // non-RFC8908 sources, such as Wi-Fi Passpoint, which can provide information such as Venue // URL, Terms & Conditions URL, and network friendly name. @@ -505,6 +508,18 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable { + networkCapabilities.getOwnerUid() + " to " + nc.getOwnerUid()); nc.setOwnerUid(networkCapabilities.getOwnerUid()); } + if (mIsAppSpecificNetwork) { + if (!networkCapabilities.getUids().equals(nc.getUids())) { + Log.e(TAG, toShortString() + ": ignoring attempt to change uid from " + + networkCapabilities.getUids() + " to " + nc.getUids()); + nc.setUids(networkCapabilities.getUids()); + } + if (nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) { + Log.e(TAG, toShortString() + ": ignoring attempt to add " + + NetworkCapabilities.capabilityNameOf(NET_CAPABILITY_NOT_RESTRICTED)); + nc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + } + } restrictCapabilitiesFromNetworkAgent(nc, creatorUid, mHasAutomotiveFeature, mConnServiceDeps, carrierPrivilegeAuthenticator); return nc; @@ -657,7 +672,8 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable { @NonNull NetworkScore score, Context context, Handler handler, NetworkAgentConfig config, ConnectivityService connService, INetd netd, IDnsResolver dnsResolver, int factorySerialNumber, int creatorUid, - int lingerDurationMs, QosCallbackTracker qosCallbackTracker, + boolean isAppSpecificNetwork, int lingerDurationMs, + QosCallbackTracker qosCallbackTracker, ConnectivityService.Dependencies deps) { Objects.requireNonNull(net); Objects.requireNonNull(info); @@ -684,6 +700,7 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable { mRegistry = new NetworkAgentMessageHandler(mHandler); this.factorySerialNumber = factorySerialNumber; this.creatorUid = creatorUid; + mIsAppSpecificNetwork = isAppSpecificNetwork; mLingerDurationMs = lingerDurationMs; mQosCallbackTracker = qosCallbackTracker; declaredUnderlyingNetworks = (nc.getUnderlyingNetworks() != null) @@ -1781,6 +1798,7 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable { + " lp{" + linkProperties + "}" + " nc{" + networkCapabilities + "}" + " factorySerialNumber=" + factorySerialNumber + + (mIsAppSpecificNetwork ? " appSpecificNetwork" : "") + "}"; } diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java index 61bcce41be..e95406b9ea 100644 --- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java @@ -13094,8 +13094,8 @@ public class ConnectivityServiceTest { nc, null /* localNetworkConfig */, new NetworkScore.Builder().setLegacyInt(0).build(), mServiceContext, null, new NetworkAgentConfig(), mService, null, null, 0, - INVALID_UID, TEST_LINGER_DELAY_MS, mQosCallbackTracker, - new ConnectivityService.Dependencies()); + INVALID_UID, false /* isAppSpecificNetwork */, TEST_LINGER_DELAY_MS, + mQosCallbackTracker, new ConnectivityService.Dependencies()); } @Test diff --git a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java index 07883ff8f8..29900dbac6 100644 --- a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java +++ b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java @@ -375,8 +375,8 @@ public class LingerMonitorTest { new LinkProperties(), caps, null /* localNetworkConfiguration */, new NetworkScore.Builder().setLegacyInt(50).build(), mCtx, null, new NetworkAgentConfig.Builder().build(), mConnService, mNetd, mDnsResolver, - NetworkProvider.ID_NONE, Binder.getCallingUid(), TEST_LINGER_DELAY_MS, - mQosCallbackTracker, new ConnectivityService.Dependencies()); + NetworkProvider.ID_NONE, Binder.getCallingUid(), false /* isAppSpecificNetwork */, + TEST_LINGER_DELAY_MS, mQosCallbackTracker, new ConnectivityService.Dependencies()); if (setEverValidated) { // As tests in this class deal with testing lingering, most tests are interested // in networks that can be lingered, and therefore must have validated in the past. diff --git a/tests/unit/java/com/android/server/connectivityservice/CSCreateAppSpecificNetworkPermissionTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSCreateAppSpecificNetworkPermissionTest.kt new file mode 100644 index 0000000000..86e43a7fe0 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivityservice/CSCreateAppSpecificNetworkPermissionTest.kt @@ -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 + +import android.content.pm.PackageManager.PERMISSION_DENIED +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED +import android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE +import android.net.NetworkRequest +import android.net.NetworkStack +import android.os.Build +import android.os.Process +import android.util.Range +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.TestableNetworkCallback +import com.android.testutils.TestableNetworkCallback.Event.CapabilitiesChanged +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(DevSdkIgnoreRunner::class) +@IgnoreUpTo(Build.VERSION_CODES.R) +class CSCreateAppSpecificNetworkPermissionTest : CSTest() { + @Test + fun testRegisterNetworkAgent() { + deps.setBuildSdk(VERSION_25Q4) + context.setPermission( + android.Manifest.permission.NETWORK_FACTORY, + PERMISSION_DENIED + ) + context.setPermission( + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + PERMISSION_DENIED + ) + context.setPermission( + android.Manifest.permission.CREATE_APP_SPECIFIC_NETWORK, + PERMISSION_GRANTED + ) + + val nc = nc(TRANSPORT_WIFI_AWARE) + nc.uids = null + nc.addCapability(NET_CAPABILITY_NOT_RESTRICTED) + val agent = Agent(nc = nc, score = keepConnectedScore()) + agent.connect() + + // Uid is set and NOT_RESTRICTED is removed + val myUid = Process.myUid() + val expectedUids = setOf(Range(myUid, myUid)) + val cb = TestableNetworkCallback() + cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb) + cb.eventuallyExpect { + it.network == agent.network && + it.caps.uids!!.equals(expectedUids) && + !it.caps.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) + } + + // Attempt to change uid and make network non-restricted is ignored + nc.setSingleUid(myUid + 1) + nc.addCapability(NET_CAPABILITY_NOT_RESTRICTED) + agent.sendNetworkCapabilities(nc) + cb.assertNoCallback() { + it is CapabilitiesChanged && it.network == agent.network && ( + !it.caps.uids!!.equals(expectedUids) || + it.caps.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) + } + } +} diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetdBypassRulesTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetdBypassRulesTests.kt index 09a3b19718..2c692dcfe1 100644 --- a/tests/unit/java/com/android/server/connectivityservice/CSNetdBypassRulesTests.kt +++ b/tests/unit/java/com/android/server/connectivityservice/CSNetdBypassRulesTests.kt @@ -76,7 +76,8 @@ class CSNetdBypassRulesTests : CSTest() { private fun createNetworkAgentInfo(network: Network): NetworkAgentInfo { return NetworkAgentInfo(na, network, info, lp, nc, localNetworkConfig?.value, score, context, csHandler, config, service, netd, dnsResolver, NetworkProvider.ID_NONE, - Binder.getCallingUid(), TEST_LINGER_DELAY_MS, qosCallbackTracker, CSDeps()) + Binder.getCallingUid(), false /* isAppSpecificNetwork */, TEST_LINGER_DELAY_MS, + qosCallbackTracker, CSDeps()) } private fun verifySetAllowBypassVpnOnNetwork( 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 40be5a5486..141a7c23ea 100644 --- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt +++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt @@ -132,7 +132,8 @@ internal const val VERSION_T = 3 internal const val VERSION_U = 4 internal const val VERSION_V = 5 internal const val VERSION_B = 6 -internal const val VERSION_MAX = VERSION_B +internal const val VERSION_25Q4 = 7 +internal const val VERSION_MAX = VERSION_25Q4 internal const val CALLING_UID_UNMOCKED = Process.INVALID_UID @@ -480,6 +481,11 @@ open class CSTest { override fun isAtLeastU() = if (isSdkUnmocked) super.isAtLeastU() else sdkLevel >= VERSION_U override fun isAtLeastV() = if (isSdkUnmocked) super.isAtLeastV() else sdkLevel >= VERSION_V override fun isAtLeastB() = if (isSdkUnmocked) super.isAtLeastB() else sdkLevel >= VERSION_B + override fun isAtLeast25Q4() = if (isSdkUnmocked) { + super.isAtLeast25Q4() + } else { + sdkLevel >= VERSION_25Q4 + } private var callingUid = CALLING_UID_UNMOCKED