summaryrefslogtreecommitdiffstats
path: root/services/core/java/com/android/server/connectivity/Vpn.java
diff options
context:
space:
mode:
Diffstat (limited to 'services/core/java/com/android/server/connectivity/Vpn.java')
-rw-r--r--services/core/java/com/android/server/connectivity/Vpn.java1205
1 files changed, 1205 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
new file mode 100644
index 0000000..2ca2cc5
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -0,0 +1,1205 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static android.Manifest.permission.BIND_VPN_SERVICE;
+
+import android.app.AppGlobals;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.net.BaseNetworkStateTracker;
+import android.net.ConnectivityManager;
+import android.net.IConnectivityManager;
+import android.net.INetworkManagementEventObserver;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.LocalSocket;
+import android.net.LocalSocketAddress;
+import android.net.NetworkInfo;
+import android.net.RouteInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.os.Binder;
+import android.os.FileUtils;
+import android.os.IBinder;
+import android.os.INetworkManagementService;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.SystemService;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.security.Credentials;
+import android.security.KeyStore;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+import android.widget.Toast;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.R;
+import com.android.internal.net.LegacyVpnInfo;
+import com.android.internal.net.VpnConfig;
+import com.android.internal.net.VpnProfile;
+import com.android.internal.util.Preconditions;
+import com.android.server.ConnectivityService.VpnCallback;
+import com.android.server.net.BaseNetworkObserver;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import libcore.io.IoUtils;
+
+/**
+ * @hide
+ */
+public class Vpn extends BaseNetworkStateTracker {
+ private static final String TAG = "Vpn";
+ private static final boolean LOGD = true;
+
+ // TODO: create separate trackers for each unique VPN to support
+ // automated reconnection
+
+ private final VpnCallback mCallback;
+
+ private String mPackage = VpnConfig.LEGACY_VPN;
+ private String mInterface;
+ private Connection mConnection;
+ private LegacyVpnRunner mLegacyVpnRunner;
+ private PendingIntent mStatusIntent;
+ private volatile boolean mEnableNotif = true;
+ private volatile boolean mEnableTeardown = true;
+ private final IConnectivityManager mConnService;
+ private VpnConfig mConfig;
+
+ /* list of users using this VPN. */
+ @GuardedBy("this")
+ private SparseBooleanArray mVpnUsers = null;
+ private BroadcastReceiver mUserIntentReceiver = null;
+
+ private final int mUserId;
+
+ public Vpn(Context context, VpnCallback callback, INetworkManagementService netService,
+ IConnectivityManager connService, int userId) {
+ // TODO: create dedicated TYPE_VPN network type
+ super(ConnectivityManager.TYPE_DUMMY);
+ mContext = context;
+ mCallback = callback;
+ mConnService = connService;
+ mUserId = userId;
+
+ try {
+ netService.registerObserver(mObserver);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Problem registering observer", e);
+ }
+ if (userId == UserHandle.USER_OWNER) {
+ // Owner's VPN also needs to handle restricted users
+ mUserIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
+ UserHandle.USER_NULL);
+ if (userId == UserHandle.USER_NULL) return;
+
+ if (Intent.ACTION_USER_ADDED.equals(action)) {
+ onUserAdded(userId);
+ } else if (Intent.ACTION_USER_REMOVED.equals(action)) {
+ onUserRemoved(userId);
+ }
+ }
+ };
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(Intent.ACTION_USER_ADDED);
+ intentFilter.addAction(Intent.ACTION_USER_REMOVED);
+ mContext.registerReceiverAsUser(
+ mUserIntentReceiver, UserHandle.ALL, intentFilter, null, null);
+ }
+ }
+
+ /**
+ * Set if this object is responsible for showing its own notifications. When
+ * {@code false}, notifications are handled externally by someone else.
+ */
+ public void setEnableNotifications(boolean enableNotif) {
+ mEnableNotif = enableNotif;
+ }
+
+ /**
+ * Set if this object is responsible for watching for {@link NetworkInfo}
+ * teardown. When {@code false}, teardown is handled externally by someone
+ * else.
+ */
+ public void setEnableTeardown(boolean enableTeardown) {
+ mEnableTeardown = enableTeardown;
+ }
+
+ @Override
+ protected void startMonitoringInternal() {
+ // Ignored; events are sent through callbacks for now
+ }
+
+ @Override
+ public boolean teardown() {
+ // TODO: finish migration to unique tracker for each VPN
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean reconnect() {
+ // TODO: finish migration to unique tracker for each VPN
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getTcpBufferSizesPropName() {
+ return PROP_TCP_BUFFER_UNKNOWN;
+ }
+
+ /**
+ * Update current state, dispaching event to listeners.
+ */
+ private void updateState(DetailedState detailedState, String reason) {
+ if (LOGD) Log.d(TAG, "setting state=" + detailedState + ", reason=" + reason);
+ mNetworkInfo.setDetailedState(detailedState, reason, null);
+ mCallback.onStateChanged(new NetworkInfo(mNetworkInfo));
+ }
+
+ /**
+ * Prepare for a VPN application. This method is designed to solve
+ * race conditions. It first compares the current prepared package
+ * with {@code oldPackage}. If they are the same, the prepared
+ * package is revoked and replaced with {@code newPackage}. If
+ * {@code oldPackage} is {@code null}, the comparison is omitted.
+ * If {@code newPackage} is the same package or {@code null}, the
+ * revocation is omitted. This method returns {@code true} if the
+ * operation is succeeded.
+ *
+ * Legacy VPN is handled specially since it is not a real package.
+ * It uses {@link VpnConfig#LEGACY_VPN} as its package name, and
+ * it can be revoked by itself.
+ *
+ * @param oldPackage The package name of the old VPN application.
+ * @param newPackage The package name of the new VPN application.
+ * @return true if the operation is succeeded.
+ */
+ public synchronized boolean prepare(String oldPackage, String newPackage) {
+ // Return false if the package does not match.
+ if (oldPackage != null && !oldPackage.equals(mPackage)) {
+ return false;
+ }
+
+ // Return true if we do not need to revoke.
+ if (newPackage == null ||
+ (newPackage.equals(mPackage) && !newPackage.equals(VpnConfig.LEGACY_VPN))) {
+ return true;
+ }
+
+ // Check if the caller is authorized.
+ enforceControlPermission();
+
+ // Reset the interface and hide the notification.
+ if (mInterface != null) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mCallback.restore();
+ final int size = mVpnUsers.size();
+ final boolean forwardDns = (mConfig.dnsServers != null &&
+ mConfig.dnsServers.size() != 0);
+ for (int i = 0; i < size; i++) {
+ int user = mVpnUsers.keyAt(i);
+ mCallback.clearUserForwarding(mInterface, user, forwardDns);
+ hideNotification(user);
+ }
+
+ mCallback.clearMarkedForwarding(mInterface);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ jniReset(mInterface);
+ mInterface = null;
+ mVpnUsers = null;
+ }
+
+ // Revoke the connection or stop LegacyVpnRunner.
+ if (mConnection != null) {
+ try {
+ mConnection.mService.transact(IBinder.LAST_CALL_TRANSACTION,
+ Parcel.obtain(), null, IBinder.FLAG_ONEWAY);
+ } catch (Exception e) {
+ // ignore
+ }
+ mContext.unbindService(mConnection);
+ mConnection = null;
+ } else if (mLegacyVpnRunner != null) {
+ mLegacyVpnRunner.exit();
+ mLegacyVpnRunner = null;
+ }
+
+ Log.i(TAG, "Switched from " + mPackage + " to " + newPackage);
+ mPackage = newPackage;
+ mConfig = null;
+ updateState(DetailedState.IDLE, "prepare");
+ return true;
+ }
+
+ /**
+ * Protect a socket from routing changes by binding it to the given
+ * interface. The socket is NOT closed by this method.
+ *
+ * @param socket The socket to be bound.
+ * @param interfaze The name of the interface.
+ */
+ public void protect(ParcelFileDescriptor socket, String interfaze) throws Exception {
+
+ PackageManager pm = mContext.getPackageManager();
+ int appUid = pm.getPackageUid(mPackage, mUserId);
+ if (Binder.getCallingUid() != appUid) {
+ throw new SecurityException("Unauthorized Caller");
+ }
+ // protect the socket from routing rules
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mCallback.protect(socket);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ // bind the socket to the interface
+ jniProtect(socket.getFd(), interfaze);
+
+ }
+
+ /**
+ * Establish a VPN network and return the file descriptor of the VPN
+ * interface. This methods returns {@code null} if the application is
+ * revoked or not prepared.
+ *
+ * @param config The parameters to configure the network.
+ * @return The file descriptor of the VPN interface.
+ */
+ public synchronized ParcelFileDescriptor establish(VpnConfig config) {
+ // Check if the caller is already prepared.
+ UserManager mgr = UserManager.get(mContext);
+ PackageManager pm = mContext.getPackageManager();
+ ApplicationInfo app = null;
+ try {
+ app = AppGlobals.getPackageManager().getApplicationInfo(mPackage, 0, mUserId);
+ if (Binder.getCallingUid() != app.uid) {
+ return null;
+ }
+ } catch (Exception e) {
+ return null;
+ }
+ // Check if the service is properly declared.
+ Intent intent = new Intent(VpnConfig.SERVICE_INTERFACE);
+ intent.setClassName(mPackage, config.user);
+ long token = Binder.clearCallingIdentity();
+ try {
+ // Restricted users are not allowed to create VPNs, they are tied to Owner
+ UserInfo user = mgr.getUserInfo(mUserId);
+ if (user.isRestricted()) {
+ throw new SecurityException("Restricted users cannot establish VPNs");
+ }
+
+ ResolveInfo info = AppGlobals.getPackageManager().resolveService(intent,
+ null, 0, mUserId);
+ if (info == null) {
+ throw new SecurityException("Cannot find " + config.user);
+ }
+ if (!BIND_VPN_SERVICE.equals(info.serviceInfo.permission)) {
+ throw new SecurityException(config.user + " does not require " + BIND_VPN_SERVICE);
+ }
+ } catch (RemoteException e) {
+ throw new SecurityException("Cannot find " + config.user);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
+ // Configure the interface. Abort if any of these steps fails.
+ ParcelFileDescriptor tun = ParcelFileDescriptor.adoptFd(jniCreate(config.mtu));
+ try {
+ updateState(DetailedState.CONNECTING, "establish");
+ String interfaze = jniGetName(tun.getFd());
+
+ // TEMP use the old jni calls until there is support for netd address setting
+ StringBuilder builder = new StringBuilder();
+ for (LinkAddress address : config.addresses) {
+ builder.append(" " + address);
+ }
+ if (jniSetAddresses(interfaze, builder.toString()) < 1) {
+ throw new IllegalArgumentException("At least one address must be specified");
+ }
+ Connection connection = new Connection();
+ if (!mContext.bindServiceAsUser(intent, connection, Context.BIND_AUTO_CREATE,
+ new UserHandle(mUserId))) {
+ throw new IllegalStateException("Cannot bind " + config.user);
+ }
+ if (mConnection != null) {
+ mContext.unbindService(mConnection);
+ }
+ if (mInterface != null && !mInterface.equals(interfaze)) {
+ jniReset(mInterface);
+ }
+ mConnection = connection;
+ mInterface = interfaze;
+
+ // Fill more values.
+ config.user = mPackage;
+ config.interfaze = mInterface;
+ config.startTime = SystemClock.elapsedRealtime();
+ mConfig = config;
+ // Set up forwarding and DNS rules.
+ mVpnUsers = new SparseBooleanArray();
+ token = Binder.clearCallingIdentity();
+ try {
+ mCallback.setMarkedForwarding(mInterface);
+ mCallback.setRoutes(interfaze, config.routes);
+ mCallback.override(mInterface, config.dnsServers, config.searchDomains);
+ addVpnUserLocked(mUserId);
+
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
+ } catch (RuntimeException e) {
+ updateState(DetailedState.FAILED, "establish");
+ IoUtils.closeQuietly(tun);
+ // make sure marked forwarding is cleared if it was set
+ try {
+ mCallback.clearMarkedForwarding(mInterface);
+ } catch (Exception ingored) {
+ // ignored
+ }
+ throw e;
+ }
+ Log.i(TAG, "Established by " + config.user + " on " + mInterface);
+
+
+ // If we are owner assign all Restricted Users to this VPN
+ if (mUserId == UserHandle.USER_OWNER) {
+ token = Binder.clearCallingIdentity();
+ try {
+ for (UserInfo user : mgr.getUsers()) {
+ if (user.isRestricted()) {
+ try {
+ addVpnUserLocked(user.id);
+ } catch (Exception e) {
+ Log.wtf(TAG, "Failed to add user " + user.id + " to owner's VPN");
+ }
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ // TODO: ensure that contract class eventually marks as connected
+ updateState(DetailedState.AUTHENTICATING, "establish");
+ return tun;
+ }
+
+ private boolean isRunningLocked() {
+ return mVpnUsers != null;
+ }
+
+ private void addVpnUserLocked(int user) {
+ enforceControlPermission();
+
+ if (!isRunningLocked()) {
+ throw new IllegalStateException("VPN is not active");
+ }
+
+ final boolean forwardDns = (mConfig.dnsServers != null &&
+ mConfig.dnsServers.size() != 0);
+
+ // add the user
+ mCallback.addUserForwarding(mInterface, user, forwardDns);
+ mVpnUsers.put(user, true);
+
+ // show the notification
+ if (!mPackage.equals(VpnConfig.LEGACY_VPN)) {
+ // Load everything for the user's notification
+ PackageManager pm = mContext.getPackageManager();
+ ApplicationInfo app = null;
+ try {
+ app = AppGlobals.getPackageManager().getApplicationInfo(mPackage, 0, mUserId);
+ } catch (RemoteException e) {
+ throw new IllegalStateException("Invalid application");
+ }
+ String label = app.loadLabel(pm).toString();
+ // Load the icon and convert it into a bitmap.
+ Drawable icon = app.loadIcon(pm);
+ Bitmap bitmap = null;
+ if (icon.getIntrinsicWidth() > 0 && icon.getIntrinsicHeight() > 0) {
+ int width = mContext.getResources().getDimensionPixelSize(
+ android.R.dimen.notification_large_icon_width);
+ int height = mContext.getResources().getDimensionPixelSize(
+ android.R.dimen.notification_large_icon_height);
+ icon.setBounds(0, 0, width, height);
+ bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(bitmap);
+ icon.draw(c);
+ c.setBitmap(null);
+ }
+ showNotification(label, bitmap, user);
+ } else {
+ showNotification(null, null, user);
+ }
+ }
+
+ private void removeVpnUserLocked(int user) {
+ enforceControlPermission();
+
+ if (!isRunningLocked()) {
+ throw new IllegalStateException("VPN is not active");
+ }
+ final boolean forwardDns = (mConfig.dnsServers != null &&
+ mConfig.dnsServers.size() != 0);
+ mCallback.clearUserForwarding(mInterface, user, forwardDns);
+ mVpnUsers.delete(user);
+ hideNotification(user);
+ }
+
+ private void onUserAdded(int userId) {
+ // If the user is restricted tie them to the owner's VPN
+ synchronized(Vpn.this) {
+ UserManager mgr = UserManager.get(mContext);
+ UserInfo user = mgr.getUserInfo(userId);
+ if (user.isRestricted()) {
+ try {
+ addVpnUserLocked(userId);
+ } catch (Exception e) {
+ Log.wtf(TAG, "Failed to add restricted user to owner", e);
+ }
+ }
+ }
+ }
+
+ private void onUserRemoved(int userId) {
+ // clean up if restricted
+ synchronized(Vpn.this) {
+ UserManager mgr = UserManager.get(mContext);
+ UserInfo user = mgr.getUserInfo(userId);
+ if (user.isRestricted()) {
+ try {
+ removeVpnUserLocked(userId);
+ } catch (Exception e) {
+ Log.wtf(TAG, "Failed to remove restricted user to owner", e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Return the configuration of the currently running VPN.
+ */
+ public VpnConfig getVpnConfig() {
+ enforceControlPermission();
+ return mConfig;
+ }
+
+ @Deprecated
+ public synchronized void interfaceStatusChanged(String iface, boolean up) {
+ try {
+ mObserver.interfaceStatusChanged(iface, up);
+ } catch (RemoteException e) {
+ // ignored; target is local
+ }
+ }
+
+ private INetworkManagementEventObserver mObserver = new BaseNetworkObserver() {
+ @Override
+ public void interfaceStatusChanged(String interfaze, boolean up) {
+ synchronized (Vpn.this) {
+ if (!up && mLegacyVpnRunner != null) {
+ mLegacyVpnRunner.check(interfaze);
+ }
+ }
+ }
+
+ @Override
+ public void interfaceRemoved(String interfaze) {
+ synchronized (Vpn.this) {
+ if (interfaze.equals(mInterface) && jniCheck(interfaze) == 0) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final int size = mVpnUsers.size();
+ final boolean forwardDns = (mConfig.dnsServers != null &&
+ mConfig.dnsServers.size() != 0);
+ for (int i = 0; i < size; i++) {
+ int user = mVpnUsers.keyAt(i);
+ mCallback.clearUserForwarding(mInterface, user, forwardDns);
+ hideNotification(user);
+ }
+ mVpnUsers = null;
+ mCallback.clearMarkedForwarding(mInterface);
+
+ mCallback.restore();
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ mInterface = null;
+ if (mConnection != null) {
+ mContext.unbindService(mConnection);
+ mConnection = null;
+ updateState(DetailedState.DISCONNECTED, "interfaceRemoved");
+ } else if (mLegacyVpnRunner != null) {
+ mLegacyVpnRunner.exit();
+ mLegacyVpnRunner = null;
+ }
+ }
+ }
+ }
+ };
+
+ private void enforceControlPermission() {
+ // System user is allowed to control VPN.
+ if (Binder.getCallingUid() == Process.SYSTEM_UID) {
+ return;
+ }
+ int appId = UserHandle.getAppId(Binder.getCallingUid());
+ final long token = Binder.clearCallingIdentity();
+ try {
+ // System VPN dialogs are also allowed to control VPN.
+ PackageManager pm = mContext.getPackageManager();
+ ApplicationInfo app = pm.getApplicationInfo(VpnConfig.DIALOGS_PACKAGE, 0);
+ if (((app.flags & ApplicationInfo.FLAG_SYSTEM) != 0) && (appId == app.uid)) {
+ return;
+ }
+ } catch (Exception e) {
+ // ignore
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
+ throw new SecurityException("Unauthorized Caller");
+ }
+
+ private class Connection implements ServiceConnection {
+ private IBinder mService;
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mService = service;
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mService = null;
+ }
+ }
+
+ private void showNotification(String label, Bitmap icon, int user) {
+ if (!mEnableNotif) return;
+ mStatusIntent = VpnConfig.getIntentForStatusPanel(mContext);
+
+ NotificationManager nm = (NotificationManager)
+ mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ if (nm != null) {
+ String title = (label == null) ? mContext.getString(R.string.vpn_title) :
+ mContext.getString(R.string.vpn_title_long, label);
+ String text = (mConfig.session == null) ? mContext.getString(R.string.vpn_text) :
+ mContext.getString(R.string.vpn_text_long, mConfig.session);
+
+ Notification notification = new Notification.Builder(mContext)
+ .setSmallIcon(R.drawable.vpn_connected)
+ .setLargeIcon(icon)
+ .setContentTitle(title)
+ .setContentText(text)
+ .setContentIntent(mStatusIntent)
+ .setDefaults(0)
+ .setOngoing(true)
+ .build();
+ nm.notifyAsUser(null, R.drawable.vpn_connected, notification, new UserHandle(user));
+ }
+ }
+
+ private void hideNotification(int user) {
+ if (!mEnableNotif) return;
+ mStatusIntent = null;
+
+ NotificationManager nm = (NotificationManager)
+ mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ if (nm != null) {
+ nm.cancelAsUser(null, R.drawable.vpn_connected, new UserHandle(user));
+ }
+ }
+
+ private native int jniCreate(int mtu);
+ private native String jniGetName(int tun);
+ private native int jniSetAddresses(String interfaze, String addresses);
+ private native int jniSetRoutes(String interfaze, String routes);
+ private native void jniReset(String interfaze);
+ private native int jniCheck(String interfaze);
+ private native void jniProtect(int socket, String interfaze);
+
+ private static RouteInfo findIPv4DefaultRoute(LinkProperties prop) {
+ for (RouteInfo route : prop.getAllRoutes()) {
+ // Currently legacy VPN only works on IPv4.
+ if (route.isDefaultRoute() && route.getGateway() instanceof Inet4Address) {
+ return route;
+ }
+ }
+
+ throw new IllegalStateException("Unable to find IPv4 default gateway");
+ }
+
+ /**
+ * Start legacy VPN, controlling native daemons as needed. Creates a
+ * secondary thread to perform connection work, returning quickly.
+ */
+ public void startLegacyVpn(VpnProfile profile, KeyStore keyStore, LinkProperties egress) {
+ enforceControlPermission();
+ if (!keyStore.isUnlocked()) {
+ throw new IllegalStateException("KeyStore isn't unlocked");
+ }
+
+ final RouteInfo ipv4DefaultRoute = findIPv4DefaultRoute(egress);
+ final String gateway = ipv4DefaultRoute.getGateway().getHostAddress();
+ final String iface = ipv4DefaultRoute.getInterface();
+
+ // Load certificates.
+ String privateKey = "";
+ String userCert = "";
+ String caCert = "";
+ String serverCert = "";
+ if (!profile.ipsecUserCert.isEmpty()) {
+ privateKey = Credentials.USER_PRIVATE_KEY + profile.ipsecUserCert;
+ byte[] value = keyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecUserCert);
+ userCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8);
+ }
+ if (!profile.ipsecCaCert.isEmpty()) {
+ byte[] value = keyStore.get(Credentials.CA_CERTIFICATE + profile.ipsecCaCert);
+ caCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8);
+ }
+ if (!profile.ipsecServerCert.isEmpty()) {
+ byte[] value = keyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecServerCert);
+ serverCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8);
+ }
+ if (privateKey == null || userCert == null || caCert == null || serverCert == null) {
+ throw new IllegalStateException("Cannot load credentials");
+ }
+
+ // Prepare arguments for racoon.
+ String[] racoon = null;
+ switch (profile.type) {
+ case VpnProfile.TYPE_L2TP_IPSEC_PSK:
+ racoon = new String[] {
+ iface, profile.server, "udppsk", profile.ipsecIdentifier,
+ profile.ipsecSecret, "1701",
+ };
+ break;
+ case VpnProfile.TYPE_L2TP_IPSEC_RSA:
+ racoon = new String[] {
+ iface, profile.server, "udprsa", privateKey, userCert,
+ caCert, serverCert, "1701",
+ };
+ break;
+ case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
+ racoon = new String[] {
+ iface, profile.server, "xauthpsk", profile.ipsecIdentifier,
+ profile.ipsecSecret, profile.username, profile.password, "", gateway,
+ };
+ break;
+ case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
+ racoon = new String[] {
+ iface, profile.server, "xauthrsa", privateKey, userCert,
+ caCert, serverCert, profile.username, profile.password, "", gateway,
+ };
+ break;
+ case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
+ racoon = new String[] {
+ iface, profile.server, "hybridrsa",
+ caCert, serverCert, profile.username, profile.password, "", gateway,
+ };
+ break;
+ }
+
+ // Prepare arguments for mtpd.
+ String[] mtpd = null;
+ switch (profile.type) {
+ case VpnProfile.TYPE_PPTP:
+ mtpd = new String[] {
+ iface, "pptp", profile.server, "1723",
+ "name", profile.username, "password", profile.password,
+ "linkname", "vpn", "refuse-eap", "nodefaultroute",
+ "usepeerdns", "idle", "1800", "mtu", "1400", "mru", "1400",
+ (profile.mppe ? "+mppe" : "nomppe"),
+ };
+ break;
+ case VpnProfile.TYPE_L2TP_IPSEC_PSK:
+ case VpnProfile.TYPE_L2TP_IPSEC_RSA:
+ mtpd = new String[] {
+ iface, "l2tp", profile.server, "1701", profile.l2tpSecret,
+ "name", profile.username, "password", profile.password,
+ "linkname", "vpn", "refuse-eap", "nodefaultroute",
+ "usepeerdns", "idle", "1800", "mtu", "1400", "mru", "1400",
+ };
+ break;
+ }
+
+ VpnConfig config = new VpnConfig();
+ config.legacy = true;
+ config.user = profile.key;
+ config.interfaze = iface;
+ config.session = profile.name;
+
+ config.addLegacyRoutes(profile.routes);
+ if (!profile.dnsServers.isEmpty()) {
+ config.dnsServers = Arrays.asList(profile.dnsServers.split(" +"));
+ }
+ if (!profile.searchDomains.isEmpty()) {
+ config.searchDomains = Arrays.asList(profile.searchDomains.split(" +"));
+ }
+ startLegacyVpn(config, racoon, mtpd);
+ }
+
+ private synchronized void startLegacyVpn(VpnConfig config, String[] racoon, String[] mtpd) {
+ stopLegacyVpn();
+
+ // Prepare for the new request. This also checks the caller.
+ prepare(null, VpnConfig.LEGACY_VPN);
+ updateState(DetailedState.CONNECTING, "startLegacyVpn");
+
+ // Start a new LegacyVpnRunner and we are done!
+ mLegacyVpnRunner = new LegacyVpnRunner(config, racoon, mtpd);
+ mLegacyVpnRunner.start();
+ }
+
+ public synchronized void stopLegacyVpn() {
+ if (mLegacyVpnRunner != null) {
+ mLegacyVpnRunner.exit();
+ mLegacyVpnRunner = null;
+
+ synchronized (LegacyVpnRunner.TAG) {
+ // wait for old thread to completely finish before spinning up
+ // new instance, otherwise state updates can be out of order.
+ }
+ }
+ }
+
+ /**
+ * Return the information of the current ongoing legacy VPN.
+ */
+ public synchronized LegacyVpnInfo getLegacyVpnInfo() {
+ // Check if the caller is authorized.
+ enforceControlPermission();
+ if (mLegacyVpnRunner == null) return null;
+
+ final LegacyVpnInfo info = new LegacyVpnInfo();
+ info.key = mConfig.user;
+ info.state = LegacyVpnInfo.stateFromNetworkInfo(mNetworkInfo);
+ if (mNetworkInfo.isConnected()) {
+ info.intent = mStatusIntent;
+ }
+ return info;
+ }
+
+ public VpnConfig getLegacyVpnConfig() {
+ if (mLegacyVpnRunner != null) {
+ return mConfig;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Bringing up a VPN connection takes time, and that is all this thread
+ * does. Here we have plenty of time. The only thing we need to take
+ * care of is responding to interruptions as soon as possible. Otherwise
+ * requests will be piled up. This can be done in a Handler as a state
+ * machine, but it is much easier to read in the current form.
+ */
+ private class LegacyVpnRunner extends Thread {
+ private static final String TAG = "LegacyVpnRunner";
+
+ private final String[] mDaemons;
+ private final String[][] mArguments;
+ private final LocalSocket[] mSockets;
+ private final String mOuterInterface;
+ private final AtomicInteger mOuterConnection =
+ new AtomicInteger(ConnectivityManager.TYPE_NONE);
+
+ private long mTimer = -1;
+
+ /**
+ * Watch for the outer connection (passing in the constructor) going away.
+ */
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!mEnableTeardown) return;
+
+ if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+ if (intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE,
+ ConnectivityManager.TYPE_NONE) == mOuterConnection.get()) {
+ NetworkInfo info = (NetworkInfo)intent.getExtra(
+ ConnectivityManager.EXTRA_NETWORK_INFO);
+ if (info != null && !info.isConnectedOrConnecting()) {
+ try {
+ mObserver.interfaceStatusChanged(mOuterInterface, false);
+ } catch (RemoteException e) {}
+ }
+ }
+ }
+ }
+ };
+
+ public LegacyVpnRunner(VpnConfig config, String[] racoon, String[] mtpd) {
+ super(TAG);
+ mConfig = config;
+ mDaemons = new String[] {"racoon", "mtpd"};
+ // TODO: clear arguments from memory once launched
+ mArguments = new String[][] {racoon, mtpd};
+ mSockets = new LocalSocket[mDaemons.length];
+
+ // This is the interface which VPN is running on,
+ // mConfig.interfaze will change to point to OUR
+ // internal interface soon. TODO - add inner/outer to mconfig
+ // TODO - we have a race - if the outer iface goes away/disconnects before we hit this
+ // we will leave the VPN up. We should check that it's still there/connected after
+ // registering
+ mOuterInterface = mConfig.interfaze;
+
+ try {
+ mOuterConnection.set(
+ mConnService.findConnectionTypeForIface(mOuterInterface));
+ } catch (Exception e) {
+ mOuterConnection.set(ConnectivityManager.TYPE_NONE);
+ }
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ mContext.registerReceiver(mBroadcastReceiver, filter);
+ }
+
+ public void check(String interfaze) {
+ if (interfaze.equals(mOuterInterface)) {
+ Log.i(TAG, "Legacy VPN is going down with " + interfaze);
+ exit();
+ }
+ }
+
+ public void exit() {
+ // We assume that everything is reset after stopping the daemons.
+ interrupt();
+ for (LocalSocket socket : mSockets) {
+ IoUtils.closeQuietly(socket);
+ }
+ updateState(DetailedState.DISCONNECTED, "exit");
+ try {
+ mContext.unregisterReceiver(mBroadcastReceiver);
+ } catch (IllegalArgumentException e) {}
+ }
+
+ @Override
+ public void run() {
+ // Wait for the previous thread since it has been interrupted.
+ Log.v(TAG, "Waiting");
+ synchronized (TAG) {
+ Log.v(TAG, "Executing");
+ execute();
+ monitorDaemons();
+ }
+ }
+
+ private void checkpoint(boolean yield) throws InterruptedException {
+ long now = SystemClock.elapsedRealtime();
+ if (mTimer == -1) {
+ mTimer = now;
+ Thread.sleep(1);
+ } else if (now - mTimer <= 60000) {
+ Thread.sleep(yield ? 200 : 1);
+ } else {
+ updateState(DetailedState.FAILED, "checkpoint");
+ throw new IllegalStateException("Time is up");
+ }
+ }
+
+ private void execute() {
+ // Catch all exceptions so we can clean up few things.
+ boolean initFinished = false;
+ try {
+ // Initialize the timer.
+ checkpoint(false);
+
+ // Wait for the daemons to stop.
+ for (String daemon : mDaemons) {
+ while (!SystemService.isStopped(daemon)) {
+ checkpoint(true);
+ }
+ }
+
+ // Clear the previous state.
+ File state = new File("/data/misc/vpn/state");
+ state.delete();
+ if (state.exists()) {
+ throw new IllegalStateException("Cannot delete the state");
+ }
+ new File("/data/misc/vpn/abort").delete();
+ initFinished = true;
+
+ // Check if we need to restart any of the daemons.
+ boolean restart = false;
+ for (String[] arguments : mArguments) {
+ restart = restart || (arguments != null);
+ }
+ if (!restart) {
+ updateState(DetailedState.DISCONNECTED, "execute");
+ return;
+ }
+ updateState(DetailedState.CONNECTING, "execute");
+
+ // Start the daemon with arguments.
+ for (int i = 0; i < mDaemons.length; ++i) {
+ String[] arguments = mArguments[i];
+ if (arguments == null) {
+ continue;
+ }
+
+ // Start the daemon.
+ String daemon = mDaemons[i];
+ SystemService.start(daemon);
+
+ // Wait for the daemon to start.
+ while (!SystemService.isRunning(daemon)) {
+ checkpoint(true);
+ }
+
+ // Create the control socket.
+ mSockets[i] = new LocalSocket();
+ LocalSocketAddress address = new LocalSocketAddress(
+ daemon, LocalSocketAddress.Namespace.RESERVED);
+
+ // Wait for the socket to connect.
+ while (true) {
+ try {
+ mSockets[i].connect(address);
+ break;
+ } catch (Exception e) {
+ // ignore
+ }
+ checkpoint(true);
+ }
+ mSockets[i].setSoTimeout(500);
+
+ // Send over the arguments.
+ OutputStream out = mSockets[i].getOutputStream();
+ for (String argument : arguments) {
+ byte[] bytes = argument.getBytes(StandardCharsets.UTF_8);
+ if (bytes.length >= 0xFFFF) {
+ throw new IllegalArgumentException("Argument is too large");
+ }
+ out.write(bytes.length >> 8);
+ out.write(bytes.length);
+ out.write(bytes);
+ checkpoint(false);
+ }
+ out.write(0xFF);
+ out.write(0xFF);
+ out.flush();
+
+ // Wait for End-of-File.
+ InputStream in = mSockets[i].getInputStream();
+ while (true) {
+ try {
+ if (in.read() == -1) {
+ break;
+ }
+ } catch (Exception e) {
+ // ignore
+ }
+ checkpoint(true);
+ }
+ }
+
+ // Wait for the daemons to create the new state.
+ while (!state.exists()) {
+ // Check if a running daemon is dead.
+ for (int i = 0; i < mDaemons.length; ++i) {
+ String daemon = mDaemons[i];
+ if (mArguments[i] != null && !SystemService.isRunning(daemon)) {
+ throw new IllegalStateException(daemon + " is dead");
+ }
+ }
+ checkpoint(true);
+ }
+
+ // Now we are connected. Read and parse the new state.
+ String[] parameters = FileUtils.readTextFile(state, 0, null).split("\n", -1);
+ if (parameters.length != 6) {
+ throw new IllegalStateException("Cannot parse the state");
+ }
+
+ // Set the interface and the addresses in the config.
+ mConfig.interfaze = parameters[0].trim();
+
+ mConfig.addLegacyAddresses(parameters[1]);
+ // Set the routes if they are not set in the config.
+ if (mConfig.routes == null || mConfig.routes.isEmpty()) {
+ mConfig.addLegacyRoutes(parameters[2]);
+ }
+
+ // Set the DNS servers if they are not set in the config.
+ if (mConfig.dnsServers == null || mConfig.dnsServers.size() == 0) {
+ String dnsServers = parameters[3].trim();
+ if (!dnsServers.isEmpty()) {
+ mConfig.dnsServers = Arrays.asList(dnsServers.split(" "));
+ }
+ }
+
+ // Set the search domains if they are not set in the config.
+ if (mConfig.searchDomains == null || mConfig.searchDomains.size() == 0) {
+ String searchDomains = parameters[4].trim();
+ if (!searchDomains.isEmpty()) {
+ mConfig.searchDomains = Arrays.asList(searchDomains.split(" "));
+ }
+ }
+
+ // Set the routes.
+ long token = Binder.clearCallingIdentity();
+ try {
+ mCallback.setMarkedForwarding(mConfig.interfaze);
+ mCallback.setRoutes(mConfig.interfaze, mConfig.routes);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
+ // Here is the last step and it must be done synchronously.
+ synchronized (Vpn.this) {
+ // Set the start time
+ mConfig.startTime = SystemClock.elapsedRealtime();
+
+ // Check if the thread is interrupted while we are waiting.
+ checkpoint(false);
+
+ // Check if the interface is gone while we are waiting.
+ if (jniCheck(mConfig.interfaze) == 0) {
+ throw new IllegalStateException(mConfig.interfaze + " is gone");
+ }
+
+ // Now INetworkManagementEventObserver is watching our back.
+ mInterface = mConfig.interfaze;
+ mVpnUsers = new SparseBooleanArray();
+
+ token = Binder.clearCallingIdentity();
+ try {
+ mCallback.override(mInterface, mConfig.dnsServers, mConfig.searchDomains);
+ addVpnUserLocked(mUserId);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
+ // Assign all restircted users to this VPN
+ // (Legacy VPNs are Owner only)
+ UserManager mgr = UserManager.get(mContext);
+ token = Binder.clearCallingIdentity();
+ try {
+ for (UserInfo user : mgr.getUsers()) {
+ if (user.isRestricted()) {
+ try {
+ addVpnUserLocked(user.id);
+ } catch (Exception e) {
+ Log.wtf(TAG, "Failed to add user " + user.id
+ + " to owner's VPN");
+ }
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ Log.i(TAG, "Connected!");
+ updateState(DetailedState.CONNECTED, "execute");
+ }
+ } catch (Exception e) {
+ Log.i(TAG, "Aborting", e);
+ // make sure the routing is cleared
+ try {
+ mCallback.clearMarkedForwarding(mConfig.interfaze);
+ } catch (Exception ignored) {
+ }
+ exit();
+ } finally {
+ // Kill the daemons if they fail to stop.
+ if (!initFinished) {
+ for (String daemon : mDaemons) {
+ SystemService.stop(daemon);
+ }
+ }
+
+ // Do not leave an unstable state.
+ if (!initFinished || mNetworkInfo.getDetailedState() == DetailedState.CONNECTING) {
+ updateState(DetailedState.FAILED, "execute");
+ }
+ }
+ }
+
+ /**
+ * Monitor the daemons we started, moving to disconnected state if the
+ * underlying services fail.
+ */
+ private void monitorDaemons() {
+ if (!mNetworkInfo.isConnected()) {
+ return;
+ }
+
+ try {
+ while (true) {
+ Thread.sleep(2000);
+ for (int i = 0; i < mDaemons.length; i++) {
+ if (mArguments[i] != null && SystemService.isStopped(mDaemons[i])) {
+ return;
+ }
+ }
+ }
+ } catch (InterruptedException e) {
+ Log.d(TAG, "interrupted during monitorDaemons(); stopping services");
+ } finally {
+ for (String daemon : mDaemons) {
+ SystemService.stop(daemon);
+ }
+
+ updateState(DetailedState.DISCONNECTED, "babysit");
+ }
+ }
+ }
+}