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:
Chiachang Wang
2022-05-05 01:58:49 +00:00
committed by Gerrit Code Review
6 changed files with 216 additions and 0 deletions

View File

@@ -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);

View File

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

View File

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

View File

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

View File

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

View File

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