Lock down networking when waiting for always-on

Fix: 26694104
Fix: 27042309
Fix: 28335277
Change-Id: I47a4c9d2b98235195b1356af3dabf7235870e4fa
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 8c4e113..32b9429 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -66,7 +66,6 @@
 import android.os.SystemService;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.provider.Settings;
 import android.security.Credentials;
 import android.security.KeyStore;
 import android.text.TextUtils;
@@ -128,6 +127,19 @@
     private final NetworkCapabilities mNetworkCapabilities;
 
     /**
+     * Whether to keep the connection active after rebooting, or upgrading or reinstalling. This
+     * only applies to {@link VpnService} connections.
+     */
+    private boolean mAlwaysOn = false;
+
+    /**
+     * Whether to disable traffic outside of this VPN even when the VPN is not connected. System
+     * apps can still bypass by choosing explicit networks. Has no effect if {@link mAlwaysOn} is
+     * not set.
+     */
+    private boolean mLockdown = false;
+
+    /**
      * List of UIDs that are set to use this VPN by default. Normally, every UID in the user is
      * added to this set but that can be changed by adding allowed or disallowed applications. It
      * is non-null iff the VPN is connected.
@@ -140,6 +152,14 @@
     @GuardedBy("this")
     private Set<UidRange> mVpnUsers = null;
 
+    /**
+     * List of UIDs for which networking should be blocked until VPN is ready, during brief periods
+     * when VPN is not running. For example, during system startup or after a crash.
+     * @see mLockdown
+     */
+    @GuardedBy("this")
+    private Set<UidRange> mBlockedUsers = new ArraySet<>();
+
     // Handle of user initiating VPN.
     private final int mUserHandle;
 
@@ -194,9 +214,10 @@
      *    manifest guarded by {@link android.Manifest.permission.BIND_VPN_SERVICE},
      *    otherwise the call will fail.
      *
-     * @param newPackage the package to designate as always-on VPN supplier.
+     * @param packageName the package to designate as always-on VPN supplier.
+     * @param lockdown whether to prevent traffic outside of a VPN, for example while connecting.
      */
-    public synchronized boolean setAlwaysOnPackage(String packageName) {
+    public synchronized boolean setAlwaysOnPackage(String packageName, boolean lockdown) {
         enforceControlPermissionOrInternalCaller();
 
         // Disconnect current VPN.
@@ -210,14 +231,9 @@
             prepareInternal(packageName);
         }
 
-        // Save the new package name in Settings.Secure.
-        final long token = Binder.clearCallingIdentity();
-        try {
-            Settings.Secure.putStringForUser(mContext.getContentResolver(),
-                    Settings.Secure.ALWAYS_ON_VPN_APP, packageName, mUserHandle);
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
+        mAlwaysOn = (packageName != null);
+        mLockdown = (mAlwaysOn && lockdown);
+        setVpnForcedLocked(mLockdown);
         return true;
     }
 
@@ -229,14 +245,7 @@
      */
     public synchronized String getAlwaysOnPackage() {
         enforceControlPermissionOrInternalCaller();
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            return Settings.Secure.getStringForUser(mContext.getContentResolver(),
-                    Settings.Secure.ALWAYS_ON_VPN_APP, mUserHandle);
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
+        return (mAlwaysOn ? mPackage : null);
     }
 
     /**
@@ -258,6 +267,11 @@
      * @return true if the operation is succeeded.
      */
     public synchronized boolean prepare(String oldPackage, String newPackage) {
+        // Stop an existing always-on VPN from being dethroned by other apps.
+        if (mAlwaysOn && !TextUtils.equals(mPackage, newPackage)) {
+            return false;
+        }
+
         if (oldPackage != null) {
             if (getAppUid(oldPackage, mUserHandle) != mOwnerUID) {
                 // The package doesn't match. We return false (to obtain user consent) unless the
@@ -281,11 +295,6 @@
             return true;
         }
 
-        // Stop an existing always-on VPN from being dethroned by other apps.
-        if (getAlwaysOnPackage() != null) {
-            return false;
-        }
-
         // Check that the caller is authorized.
         enforceControlPermission();
 
@@ -469,7 +478,7 @@
         mNetworkInfo.setDetailedState(DetailedState.CONNECTING, null, null);
 
         NetworkMisc networkMisc = new NetworkMisc();
-        networkMisc.allowBypass = mConfig.allowBypass;
+        networkMisc.allowBypass = mConfig.allowBypass && !mLockdown;
 
         long token = Binder.clearCallingIdentity();
         try {
@@ -685,7 +694,7 @@
             final long token = Binder.clearCallingIdentity();
             List<UserInfo> users;
             try {
-                users = UserManager.get(mContext).getUsers();
+                users = UserManager.get(mContext).getUsers(true);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
@@ -774,18 +783,22 @@
     public void onUserAdded(int userHandle) {
         // If the user is restricted tie them to the parent user's VPN
         UserInfo user = UserManager.get(mContext).getUserInfo(userHandle);
-        if (user.isRestricted() && user.restrictedProfileParentId == mUserHandle
-                && mVpnUsers != null) {
+        if (user.isRestricted() && user.restrictedProfileParentId == mUserHandle) {
             synchronized(Vpn.this) {
-                try {
-                    addUserToRanges(mVpnUsers, userHandle, mConfig.allowedApplications,
-                            mConfig.disallowedApplications);
-                    if (mNetworkAgent != null) {
-                        final List<UidRange> ranges = uidRangesForUser(userHandle);
-                        mNetworkAgent.addUidRanges(ranges.toArray(new UidRange[ranges.size()]));
+                if (mVpnUsers != null) {
+                    try {
+                        addUserToRanges(mVpnUsers, userHandle, mConfig.allowedApplications,
+                                mConfig.disallowedApplications);
+                        if (mNetworkAgent != null) {
+                            final List<UidRange> ranges = uidRangesForUser(userHandle);
+                            mNetworkAgent.addUidRanges(ranges.toArray(new UidRange[ranges.size()]));
+                        }
+                    } catch (Exception e) {
+                        Log.wtf(TAG, "Failed to add restricted user to owner", e);
                     }
-                } catch (Exception e) {
-                    Log.wtf(TAG, "Failed to add restricted user to owner", e);
+                }
+                if (mAlwaysOn) {
+                    setVpnForcedLocked(mLockdown);
                 }
             }
         }
@@ -794,19 +807,101 @@
     public void onUserRemoved(int userHandle) {
         // clean up if restricted
         UserInfo user = UserManager.get(mContext).getUserInfo(userHandle);
-        if (user.isRestricted() && user.restrictedProfileParentId == mUserHandle
-                && mVpnUsers != null) {
+        if (user.isRestricted() && user.restrictedProfileParentId == mUserHandle) {
             synchronized(Vpn.this) {
-                try {
-                    removeVpnUserLocked(userHandle);
-                } catch (Exception e) {
-                    Log.wtf(TAG, "Failed to remove restricted user to owner", e);
+                if (mVpnUsers != null) {
+                    try {
+                        removeVpnUserLocked(userHandle);
+                    } catch (Exception e) {
+                        Log.wtf(TAG, "Failed to remove restricted user to owner", e);
+                    }
+                }
+                if (mAlwaysOn) {
+                    setVpnForcedLocked(mLockdown);
                 }
             }
         }
     }
 
     /**
+     * Called when the user associated with this VPN has just been stopped.
+     */
+    public synchronized void onUserStopped() {
+        // Switch off networking lockdown (if it was enabled)
+        setVpnForcedLocked(false);
+        mAlwaysOn = false;
+
+        // Quit any active connections
+        agentDisconnect();
+    }
+
+    /**
+     * Restrict network access from all UIDs affected by this {@link Vpn}, apart from the VPN
+     * service app itself, to only sockets that have had {@code protect()} called on them. All
+     * non-VPN traffic is blocked via a {@code PROHIBIT} response from the kernel.
+     *
+     * The exception for the VPN UID isn't technically necessary -- setup should use protected
+     * sockets -- but in practice it saves apps that don't protect their sockets from breaking.
+     *
+     * Calling multiple times with {@param enforce} = {@code true} will recreate the set of UIDs to
+     * block every time, and if anything has changed update using {@link #setAllowOnlyVpnForUids}.
+     *
+     * @param enforce {@code true} to require that all traffic under the jurisdiction of this
+     *                {@link Vpn} goes through a VPN connection or is blocked until one is
+     *                available, {@code false} to lift the requirement.
+     *
+     * @see #mBlockedUsers
+     */
+    @GuardedBy("this")
+    private void setVpnForcedLocked(boolean enforce) {
+        final Set<UidRange> removedRanges = new ArraySet<>(mBlockedUsers);
+        if (enforce) {
+            final Set<UidRange> addedRanges = createUserAndRestrictedProfilesRanges(mUserHandle,
+                    /* allowedApplications */ null,
+                    /* disallowedApplications */ Collections.singletonList(mPackage));
+
+            removedRanges.removeAll(addedRanges);
+            addedRanges.removeAll(mBlockedUsers);
+
+            setAllowOnlyVpnForUids(false, removedRanges);
+            setAllowOnlyVpnForUids(true, addedRanges);
+        } else {
+            setAllowOnlyVpnForUids(false, removedRanges);
+        }
+    }
+
+    /**
+     * Either add or remove a list of {@link UidRange}s to the list of UIDs that are only allowed
+     * to make connections through sockets that have had {@code protect()} called on them.
+     *
+     * @param enforce {@code true} to add to the blacklist, {@code false} to remove.
+     * @param ranges {@link Collection} of {@link UidRange}s to add (if {@param enforce} is
+     *               {@code true}) or to remove.
+     * @return {@code true} if all of the UIDs were added/removed. {@code false} otherwise,
+     *         including added ranges that already existed or removed ones that didn't.
+     */
+    @GuardedBy("this")
+    private boolean setAllowOnlyVpnForUids(boolean enforce, Collection<UidRange> ranges) {
+        if (ranges.size() == 0) {
+            return true;
+        }
+        final UidRange[] rangesArray = ranges.toArray(new UidRange[ranges.size()]);
+        try {
+            mNetd.setAllowOnlyVpnForUids(enforce, rangesArray);
+        } catch (RemoteException | RuntimeException e) {
+            Log.e(TAG, "Updating blocked=" + enforce
+                    + " for UIDs " + Arrays.toString(ranges.toArray()) + " failed", e);
+            return false;
+        }
+        if (enforce) {
+            mBlockedUsers.addAll(ranges);
+        } else {
+            mBlockedUsers.removeAll(ranges);
+        }
+        return true;
+    }
+
+    /**
      * Return the configuration of the currently running VPN.
      */
     public VpnConfig getVpnConfig() {
@@ -959,6 +1054,21 @@
         return false;
     }
 
+    /**
+     * @return {@code true} if the set of users blocked whilst waiting for VPN to connect includes
+     *         the UID {@param uid}, {@code false} otherwise.
+     *
+     * @see #mBlockedUsers
+     */
+    public synchronized boolean isBlockingUid(int uid) {
+        for (UidRange uidRange : mBlockedUsers) {
+            if (uidRange.contains(uid)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private native int jniCreate(int mtu);
     private native String jniGetName(int tun);
     private native int jniSetAddresses(String interfaze, String addresses);