Merge changes from topic "AppExclusion"
* changes: Allow getting/setting app exclusion list for specific vpn profile Add to/from disk stable format
This commit is contained in:
@@ -42,6 +42,8 @@ interface IVpnManager {
|
||||
String startVpnProfile(String packageName);
|
||||
void stopVpnProfile(String packageName);
|
||||
VpnProfileState getProvisionedVpnProfileState(String packageName);
|
||||
boolean setAppExclusionList(int userId, String vpnPackage, in List<String> excludedApps);
|
||||
List<String> getAppExclusionList(int userId, String vpnPackage);
|
||||
|
||||
/** Always-on VPN APIs */
|
||||
boolean isAlwaysOnVpnPackageSupported(int userId, String packageName);
|
||||
|
||||
@@ -594,6 +594,63 @@ public class VpnManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the application exclusion list for the specified VPN profile.
|
||||
*
|
||||
* <p>If an app in the set of excluded apps is not installed for the given user, it will be
|
||||
* skipped in the list of app exclusions. If apps are installed or removed, any active VPN will
|
||||
* have its UID set updated automatically. If the caller is not {@code userId},
|
||||
* {@link android.Manifest.permission.INTERACT_ACROSS_USERS_FULL} permission is required.
|
||||
*
|
||||
* <p>This will ONLY affect VpnManager profiles. As such, the NETWORK_SETTINGS provider MUST NOT
|
||||
* allow configuration of these options if the application has not provided a VPN profile.
|
||||
*
|
||||
* @param userId the identifier of the user to set app exclusion list
|
||||
* @param vpnPackage The package name for an installed VPN app on the device
|
||||
* @param excludedApps the app exclusion list
|
||||
* @throws IllegalStateException exception if vpn for the @code userId} is not ready yet.
|
||||
*
|
||||
* @return whether setting the list is successful or not
|
||||
* @hide
|
||||
*/
|
||||
@RequiresPermission(anyOf = {
|
||||
android.Manifest.permission.NETWORK_SETTINGS,
|
||||
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
|
||||
android.Manifest.permission.NETWORK_STACK})
|
||||
public boolean setAppExclusionList(int userId, @NonNull String vpnPackage,
|
||||
@NonNull List<String> excludedApps) {
|
||||
try {
|
||||
return mService.setAppExclusionList(userId, vpnPackage, excludedApps);
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the application exclusion list for the specified VPN profile. If the caller is not
|
||||
* {@code userId}, {@link android.Manifest.permission.INTERACT_ACROSS_USERS_FULL} permission
|
||||
* is required.
|
||||
*
|
||||
* @param userId the identifier of the user to set app exclusion list
|
||||
* @param vpnPackage The package name for an installed VPN app on the device
|
||||
* @return the list of packages for the specified VPN profile or null if no corresponding VPN
|
||||
* profile configured.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RequiresPermission(anyOf = {
|
||||
android.Manifest.permission.NETWORK_SETTINGS,
|
||||
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
|
||||
android.Manifest.permission.NETWORK_STACK})
|
||||
@Nullable
|
||||
public List<String> getAppExclusionList(int userId, @NonNull String vpnPackage) {
|
||||
try {
|
||||
return mService.getAppExclusionList(userId, vpnPackage);
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the list of packages that are allowed to access network when always-on VPN is in
|
||||
* lockdown mode but not connected. Returns {@code null} when VPN lockdown is not active.
|
||||
|
||||
@@ -880,6 +880,38 @@ public class VpnManagerService extends IVpnManager.Stub {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setAppExclusionList(int userId, String vpnPackage, List<String> excludedApps) {
|
||||
enforceSettingsPermission();
|
||||
enforceCrossUserPermission(userId);
|
||||
|
||||
synchronized (mVpns) {
|
||||
final Vpn vpn = mVpns.get(userId);
|
||||
if (vpn != null) {
|
||||
return vpn.setAppExclusionList(vpnPackage, excludedApps);
|
||||
} else {
|
||||
logw("User " + userId + " has no Vpn configuration");
|
||||
throw new IllegalStateException(
|
||||
"VPN for user " + userId + " not ready yet. Skipping setting the list");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAppExclusionList(int userId, String vpnPackage) {
|
||||
enforceSettingsPermission();
|
||||
enforceCrossUserPermission(userId);
|
||||
|
||||
synchronized (mVpns) {
|
||||
final Vpn vpn = mVpns.get(userId);
|
||||
if (vpn != null) {
|
||||
return vpn.getAppExclusionList(vpnPackage);
|
||||
} else {
|
||||
logw("User " + userId + " has no Vpn configuration");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void factoryReset() {
|
||||
|
||||
@@ -27,6 +27,8 @@ import static android.net.VpnManager.NOTIFICATION_CHANNEL_VPN;
|
||||
import static android.os.PowerWhitelistManager.REASON_VPN;
|
||||
import static android.os.UserHandle.PER_USER_RANGE;
|
||||
|
||||
import static com.android.server.vcn.util.PersistableBundleUtils.STRING_DESERIALIZER;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import android.Manifest;
|
||||
@@ -100,6 +102,7 @@ import android.os.INetworkManagementService;
|
||||
import android.os.Looper;
|
||||
import android.os.Parcel;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.PersistableBundle;
|
||||
import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
import android.os.SystemClock;
|
||||
@@ -131,6 +134,7 @@ import com.android.net.module.util.NetworkStackConstants;
|
||||
import com.android.server.DeviceIdleInternal;
|
||||
import com.android.server.LocalServices;
|
||||
import com.android.server.net.BaseNetworkObserver;
|
||||
import com.android.server.vcn.util.PersistableBundleUtils;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
@@ -178,6 +182,8 @@ public class Vpn {
|
||||
private static final String VPN_PROVIDER_NAME_BASE = "VpnNetworkProvider:";
|
||||
private static final boolean LOGD = true;
|
||||
private static final String ANDROID_KEYSTORE_PROVIDER = "AndroidKeyStore";
|
||||
/** Key containing prefix of vpn app excluded list */
|
||||
@VisibleForTesting static final String VPN_APP_EXCLUDED = "VPN_APP_EXCLUDED_";
|
||||
|
||||
// Length of time (in milliseconds) that an app hosting an always-on VPN is placed on
|
||||
// the device idle allowlist during service launch and VPN bootstrap.
|
||||
@@ -2727,6 +2733,8 @@ public class Vpn {
|
||||
|
||||
mConfig.underlyingNetworks = new Network[] {network};
|
||||
|
||||
mConfig.disallowedApplications = getAppExclusionList(mPackage);
|
||||
|
||||
networkAgent = mNetworkAgent;
|
||||
|
||||
// The below must be done atomically with the mConfig update, otherwise
|
||||
@@ -3681,6 +3689,88 @@ public class Vpn {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean storeAppExclusionList(@NonNull String packageName,
|
||||
@NonNull List<String> excludedApps) {
|
||||
byte[] data;
|
||||
try {
|
||||
final PersistableBundle bundle = PersistableBundleUtils.fromList(
|
||||
excludedApps, PersistableBundleUtils.STRING_SERIALIZER);
|
||||
data = PersistableBundleUtils.toDiskStableBytes(bundle);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "problem writing into stream", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
final long oldId = Binder.clearCallingIdentity();
|
||||
try {
|
||||
getVpnProfileStore().put(getVpnAppExcludedForPackage(packageName), data);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(oldId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
String getVpnAppExcludedForPackage(String packageName) {
|
||||
return VPN_APP_EXCLUDED + mUserId + "_" + packageName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the application exclusion list for the specified VPN profile.
|
||||
*
|
||||
* @param packageName the package name of the app provisioning this profile
|
||||
* @param excludedApps the list of excluded packages
|
||||
*
|
||||
* @return whether setting the list is successful or not
|
||||
*/
|
||||
public synchronized boolean setAppExclusionList(@NonNull String packageName,
|
||||
@NonNull List<String> excludedApps) {
|
||||
enforceNotRestrictedUser();
|
||||
if (!storeAppExclusionList(packageName, excludedApps)) return false;
|
||||
// Re-build and update NetworkCapabilities via NetworkAgent.
|
||||
if (mNetworkAgent != null) {
|
||||
// Only update the platform VPN
|
||||
if (isIkev2VpnRunner()) {
|
||||
mConfig.disallowedApplications = List.copyOf(excludedApps);
|
||||
mNetworkCapabilities = new NetworkCapabilities.Builder(mNetworkCapabilities)
|
||||
.setUids(createUserAndRestrictedProfilesRanges(
|
||||
mUserId, null /* allowedApplications */, excludedApps))
|
||||
.build();
|
||||
mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the application exclusion list for the specified VPN profile.
|
||||
*
|
||||
* @param packageName the package name of the app provisioning this profile
|
||||
* @return the list of excluded packages for the specified VPN profile or empty list if there is
|
||||
* no provisioned VPN profile.
|
||||
*/
|
||||
@NonNull
|
||||
public synchronized List<String> getAppExclusionList(@NonNull String packageName) {
|
||||
enforceNotRestrictedUser();
|
||||
|
||||
final long oldId = Binder.clearCallingIdentity();
|
||||
try {
|
||||
final byte[] bytes = getVpnProfileStore().get(getVpnAppExcludedForPackage(packageName));
|
||||
|
||||
if (bytes == null || bytes.length == 0) return new ArrayList<>();
|
||||
|
||||
final PersistableBundle bundle = PersistableBundleUtils.fromDiskStableBytes(bytes);
|
||||
return PersistableBundleUtils.toList(bundle, STRING_DESERIALIZER);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "problem reading from stream", e);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(oldId);
|
||||
}
|
||||
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
private @VpnProfileState.State int getStateFromLegacyState(int legacyState) {
|
||||
switch (legacyState) {
|
||||
case LegacyVpnInfo.STATE_CONNECTING:
|
||||
|
||||
@@ -23,6 +23,8 @@ import android.os.PersistableBundle;
|
||||
|
||||
import com.android.internal.util.HexDump;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
@@ -295,6 +297,30 @@ public class PersistableBundleUtils {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a PersistableBundle into a disk-stable byte array format
|
||||
*
|
||||
* @param bundle the PersistableBundle to be converted to a disk-stable format
|
||||
* @return the byte array representation of the PersistableBundle
|
||||
*/
|
||||
@Nullable
|
||||
public static byte[] toDiskStableBytes(@NonNull PersistableBundle bundle) throws IOException {
|
||||
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
bundle.writeToStream(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts from a disk-stable byte array format to a PersistableBundle
|
||||
*
|
||||
* @param bytes the disk-stable byte array
|
||||
* @return the PersistableBundle parsed from this byte array.
|
||||
*/
|
||||
public static PersistableBundle fromDiskStableBytes(@NonNull byte[] bytes) throws IOException {
|
||||
final ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
|
||||
return PersistableBundle.readFromStream(inputStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures safe reading and writing of {@link PersistableBundle}s to and from disk.
|
||||
*
|
||||
|
||||
@@ -267,6 +267,15 @@ public class PersistableBundleUtilsTest {
|
||||
assertTrue(PersistableBundleUtils.isEqual(testBundle, minimized));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToFromDiskStableBytes() throws Exception {
|
||||
final PersistableBundle testBundle = getTestBundle();
|
||||
final PersistableBundle result =
|
||||
PersistableBundleUtils.fromDiskStableBytes(
|
||||
PersistableBundleUtils.toDiskStableBytes(testBundle));
|
||||
assertTrue(PersistableBundleUtils.isEqual(testBundle, result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEquality_identical() throws Exception {
|
||||
final PersistableBundle left = getTestBundle();
|
||||
|
||||
Reference in New Issue
Block a user