diff --git a/api/current.txt b/api/current.txt
index d25ae966d9079..b5c876b6d1798 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -17443,6 +17443,7 @@ package android.net {
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 @@ package android.net {
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 a7bbc53366f73..adc16f1a5c482 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -170,4 +170,5 @@ interface IConnectivityManager
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 d469487a7a0f3..ad549128ea299 100644
--- a/core/java/android/net/VpnService.java
+++ b/core/java/android/net/VpnService.java
@@ -27,6 +27,7 @@ import android.content.Context;
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;
@@ -287,6 +288,46 @@ public class VpnService extends Service {
}
}
+ /**
+ * 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:
+ *
+ * - a non-empty array: 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.
+ * - an empty array: a zero-element array, meaning that the VPN has no
+ * underlying network connection, and thus, app traffic will not be sent or received.
+ * - null: (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.
+ *
+ *
+ * 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}
@@ -662,6 +703,20 @@ public class VpnService extends Service {
return this;
}
+ /**
+ * 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
diff --git a/core/java/com/android/internal/net/VpnConfig.java b/core/java/com/android/internal/net/VpnConfig.java
index 3d016be982093..c5d9db486f982 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.PackageManager.NameNotFoundException;
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 class VpnConfig implements Parcelable {
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 @@ public class VpnConfig implements Parcelable {
out.writeInt(allowBypass ? 1 : 0);
out.writeInt(allowIPv4 ? 1 : 0);
out.writeInt(allowIPv6 ? 1 : 0);
+ out.writeTypedArray(underlyingNetworks, flags);
}
public static final Parcelable.Creator CREATOR =
@@ -186,6 +189,7 @@ public class VpnConfig implements Parcelable {
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 ec0e15c2d5c62..b1e932d388619 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -865,6 +865,29 @@ public class ConnectivityService extends IConnectivityManager.Stub
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 @@ public class ConnectivityService extends IConnectivityManager.Stub
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 03c05ecd464da..6da186f04320a 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.LinkAddress;
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 @@ public class Vpn {
}
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 @@ public class Vpn {
// 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 @@ public class Vpn {
}
private void removeVpnUserLocked(int userHandle) {
- if (!isRunningLocked()) {
+ if (mVpnUsers == null) {
throw new IllegalStateException("VPN is not active");
}
final List ranges = uidRangesForUser(userHandle);
@@ -767,27 +774,61 @@ public class Vpn {
}
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);