Allow VPNs to specify their underlying networks.

These are used when responding to getActiveNetworkInfo() (and cousins)
when an app is subject to the VPN.

Bug: 17460017
Change-Id: Ief7a840c760777a41d3358aa6b8e4cdd99c29f24
diff --git a/api/current.txt b/api/current.txt
index d25ae96..b5c876b 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -17443,6 +17443,7 @@
     method public boolean protect(int);
     method public boolean protect(java.net.Socket);
     method public boolean protect(java.net.DatagramSocket);
+    method public boolean setUnderlyingNetworks(android.net.Network[]);
     field public static final java.lang.String SERVICE_INTERFACE = "android.net.VpnService";
   }
 
@@ -17464,6 +17465,7 @@
     method public android.net.VpnService.Builder setConfigureIntent(android.app.PendingIntent);
     method public android.net.VpnService.Builder setMtu(int);
     method public android.net.VpnService.Builder setSession(java.lang.String);
+    method public android.net.VpnService.Builder setUnderlyingNetworks(android.net.Network[]);
   }
 
 }
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index a7bbc53..adc16f1 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -170,4 +170,5 @@
 
     boolean addVpnAddress(String address, int prefixLength);
     boolean removeVpnAddress(String address, int prefixLength);
+    boolean setUnderlyingNetworksForVpn(in Network[] networks);
 }
diff --git a/core/java/android/net/VpnService.java b/core/java/android/net/VpnService.java
index d469487..ad54912 100644
--- a/core/java/android/net/VpnService.java
+++ b/core/java/android/net/VpnService.java
@@ -27,6 +27,7 @@
 import android.content.Intent;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
+import android.net.Network;
 import android.net.NetworkUtils;
 import android.os.Binder;
 import android.os.IBinder;
@@ -288,6 +289,46 @@
     }
 
     /**
+     * Sets the underlying networks used by the VPN for its upstream connections.
+     *
+     * Used by the system to know the actual networks that carry traffic for apps affected by this
+     * VPN in order to present this information to the user (e.g., via status bar icons).
+     *
+     * This method only needs to be called if the VPN has explicitly bound its underlying
+     * communications channels — such as the socket(s) passed to {@link #protect(int)} —
+     * to a {@code Network} using APIs such as {@link Network#bindSocket} or {@link
+     * Network#bindDatagramSocket}. The VPN should call this method every time the set of {@code
+     * Network}s it is using changes.
+     *
+     * {@code networks} is one of the following:
+     * <ul>
+     * <li><strong>a non-empty array</strong>: an array of one or more {@link Network}s, in
+     * decreasing preference order. For example, if this VPN uses both wifi and mobile (cellular)
+     * networks to carry app traffic, but prefers or uses wifi more than mobile, wifi should appear
+     * first in the array.</li>
+     * <li><strong>an empty array</strong>: a zero-element array, meaning that the VPN has no
+     * underlying network connection, and thus, app traffic will not be sent or received.</li>
+     * <li><strong>null</strong>: (default) signifies that the VPN uses whatever is the system's
+     * default network. I.e., it doesn't use the {@code bindSocket} or {@code bindDatagramSocket}
+     * APIs mentioned above to send traffic over specific channels.
+     * </ul>
+     *
+     * This call will succeed only if the VPN is currently established. For setting this value when
+     * the VPN has not yet been established, see {@link Builder#setUnderlyingNetworks}.
+     *
+     * @param networks An array of networks the VPN uses to tunnel traffic to/from its servers.
+     *
+     * @return {@code true} on success.
+     */
+    public boolean setUnderlyingNetworks(Network[] networks) {
+        try {
+            return getService().setUnderlyingNetworksForVpn(networks);
+        } catch (RemoteException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    /**
      * Return the communication interface to the service. This method returns
      * {@code null} on {@link Intent}s other than {@link #SERVICE_INTERFACE}
      * action. Applications overriding this method must identify the intent
@@ -663,6 +704,20 @@
         }
 
         /**
+         * Sets the underlying networks used by the VPN for its upstream connections.
+         *
+         * @see VpnService#setUnderlyingNetworks
+         *
+         * @param networks An array of networks the VPN uses to tunnel traffic to/from its servers.
+         *
+         * @return this {@link Builder} object to facilitate chaining method calls.
+         */
+        public Builder setUnderlyingNetworks(Network[] networks) {
+            mConfig.underlyingNetworks = networks != null ? networks.clone() : null;
+            return this;
+        }
+
+        /**
          * Create a VPN interface using the parameters supplied to this
          * builder. The interface works on IP packets, and a file descriptor
          * is returned for the application to access them. Each read
diff --git a/core/java/com/android/internal/net/VpnConfig.java b/core/java/com/android/internal/net/VpnConfig.java
index 3d016be..c5d9db4 100644
--- a/core/java/com/android/internal/net/VpnConfig.java
+++ b/core/java/com/android/internal/net/VpnConfig.java
@@ -25,6 +25,7 @@
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.net.LinkAddress;
+import android.net.Network;
 import android.net.RouteInfo;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -99,6 +100,7 @@
     public boolean allowBypass;
     public boolean allowIPv4;
     public boolean allowIPv6;
+    public Network[] underlyingNetworks;
 
     public void updateAllowedFamilies(InetAddress address) {
         if (address instanceof Inet4Address) {
@@ -162,6 +164,7 @@
         out.writeInt(allowBypass ? 1 : 0);
         out.writeInt(allowIPv4 ? 1 : 0);
         out.writeInt(allowIPv6 ? 1 : 0);
+        out.writeTypedArray(underlyingNetworks, flags);
     }
 
     public static final Parcelable.Creator<VpnConfig> CREATOR =
@@ -186,6 +189,7 @@
             config.allowBypass = in.readInt() != 0;
             config.allowIPv4 = in.readInt() != 0;
             config.allowIPv6 = in.readInt() != 0;
+            config.underlyingNetworks = in.createTypedArray(Network.CREATOR);
             return config;
         }
 
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index ec0e15c..b1e932d 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -865,6 +865,29 @@
         Network network = null;
 
         NetworkAgentInfo nai = mNetworkForRequestId.get(mDefaultRequest.requestId);
+
+        if (!mLockdownEnabled) {
+            int user = UserHandle.getUserId(uid);
+            synchronized (mVpns) {
+                Vpn vpn = mVpns.get(user);
+                if (vpn != null && vpn.appliesToUid(uid)) {
+                    // getUnderlyingNetworks() returns:
+                    // null => the VPN didn't specify anything, so we use the default.
+                    // empty array => the VPN explicitly said "no default network".
+                    // non-empty array => the VPN specified one or more default networks; we use the
+                    //                    first one.
+                    Network[] networks = vpn.getUnderlyingNetworks();
+                    if (networks != null) {
+                        if (networks.length > 0) {
+                            nai = getNetworkAgentInfoForNetwork(networks[0]);
+                        } else {
+                            nai = null;
+                        }
+                    }
+                }
+            }
+        }
+
         if (nai != null) {
             synchronized (nai) {
                 info = new NetworkInfo(nai.networkInfo);
@@ -4376,4 +4399,13 @@
             return mVpns.get(user).removeAddress(address, prefixLength);
         }
     }
+
+    @Override
+    public boolean setUnderlyingNetworksForVpn(Network[] networks) {
+        throwIfLockdownEnabled();
+        int user = UserHandle.getUserId(Binder.getCallingUid());
+        synchronized (mVpns) {
+            return mVpns.get(user).setUnderlyingNetworks(networks);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 03c05ec..6da186f 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -46,6 +46,7 @@
 import android.net.LinkProperties;
 import android.net.LocalSocket;
 import android.net.LocalSocketAddress;
+import android.net.Network;
 import android.net.NetworkAgent;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
@@ -580,7 +581,13 @@
     }
 
     private boolean isRunningLocked() {
-        return mVpnUsers != null;
+        return mNetworkAgent != null && mInterface != null;
+    }
+
+    // Returns true if the VPN has been established and the calling UID is its owner. Used to check
+    // that a call to mutate VPN state is admissible.
+    private boolean isCallerEstablishedOwnerLocked() {
+        return isRunningLocked() && Binder.getCallingUid() == mOwnerUID;
     }
 
     // Note: Return type guarantees results are deduped and sorted, which callers require.
@@ -595,7 +602,7 @@
 
     // Note: This function adds to mVpnUsers but does not publish list to NetworkAgent.
     private void addVpnUserLocked(int userHandle) {
-        if (!isRunningLocked()) {
+        if (mVpnUsers == null) {
             throw new IllegalStateException("VPN is not active");
         }
 
@@ -647,7 +654,7 @@
     }
 
     private void removeVpnUserLocked(int userHandle) {
-        if (!isRunningLocked()) {
+        if (mVpnUsers == null) {
             throw new IllegalStateException("VPN is not active");
         }
         final List<UidRange> ranges = uidRangesForUser(userHandle);
@@ -767,27 +774,61 @@
     }
 
     public synchronized boolean addAddress(String address, int prefixLength) {
-        if (Binder.getCallingUid() != mOwnerUID || mInterface == null || mNetworkAgent == null) {
+        if (!isCallerEstablishedOwnerLocked()) {
             return false;
         }
         boolean success = jniAddAddress(mInterface, address, prefixLength);
-        if (mNetworkAgent != null) {
-            mNetworkAgent.sendLinkProperties(makeLinkProperties());
-        }
+        mNetworkAgent.sendLinkProperties(makeLinkProperties());
         return success;
     }
 
     public synchronized boolean removeAddress(String address, int prefixLength) {
-        if (Binder.getCallingUid() != mOwnerUID || mInterface == null || mNetworkAgent == null) {
+        if (!isCallerEstablishedOwnerLocked()) {
             return false;
         }
         boolean success = jniDelAddress(mInterface, address, prefixLength);
-        if (mNetworkAgent != null) {
-            mNetworkAgent.sendLinkProperties(makeLinkProperties());
-        }
+        mNetworkAgent.sendLinkProperties(makeLinkProperties());
         return success;
     }
 
+    public synchronized boolean setUnderlyingNetworks(Network[] networks) {
+        if (!isCallerEstablishedOwnerLocked()) {
+            return false;
+        }
+        if (networks == null) {
+            mConfig.underlyingNetworks = null;
+        } else {
+            mConfig.underlyingNetworks = new Network[networks.length];
+            for (int i = 0; i < networks.length; ++i) {
+                if (networks[i] == null) {
+                    mConfig.underlyingNetworks[i] = null;
+                } else {
+                    mConfig.underlyingNetworks[i] = new Network(networks[i].netId);
+                }
+            }
+        }
+        return true;
+    }
+
+    public synchronized Network[] getUnderlyingNetworks() {
+        if (!isRunningLocked()) {
+            return null;
+        }
+        return mConfig.underlyingNetworks;
+    }
+
+    public synchronized boolean appliesToUid(int uid) {
+        if (!isRunningLocked()) {
+            return false;
+        }
+        for (UidRange uidRange : mVpnUsers) {
+            if (uidRange.start <= uid && uid <= uidRange.stop) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private native int jniCreate(int mtu);
     private native String jniGetName(int tun);
     private native int jniSetAddresses(String interfaze, String addresses);