Allow app-specific network creation with a new permission

If an app registers a network agent and doesn't have NETWORK_FACTORY or
PERMISSION_MAINLINE_NETWORK_STACK, but does have
CREATE_APP_SPECIFIC_NETWORK, ConnectivityService will:
- Allow it to create a network
- Force the capabilities to contain only the app's UID
- Force the network to be restricted
- Ignore attempts to change the UID or make the network non-restricted

Test: TH
Flag: android.net.platform.flags.create_app_specific_network_permission
Bug: 435313135
Change-Id: I5a9517515e64f799c88aca4a51636b1a08e15817
This commit is contained in:
Motomu Utsumi 2025-07-28 15:32:46 +09:00
commit 72d4b516c9
8 changed files with 158 additions and 17 deletions

View file

@ -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,

View file

@ -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();
@ -2303,7 +2307,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
@ -4090,14 +4094,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;
@ -9864,10 +9882,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);
@ -9888,7 +9915,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);
}
@ -9899,7 +9926,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.
@ -9908,6 +9935,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
@ -9916,7 +9949,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)

View file

@ -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" : "")
+ "}";
}

View file

@ -13089,8 +13089,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

View file

@ -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.

View file

@ -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<CapabilitiesChanged> {
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))
}
}
}

View file

@ -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(

View file

@ -131,7 +131,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
@ -478,6 +479,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