The service part of the user space VPN support.

The dialogs will be in another change.

Change-Id: I0cdfd2ef21ffd40ee955b3cbde5ada65dbfdb0bc
diff --git a/services/java/com/android/server/ConnectivityService.java b/services/java/com/android/server/ConnectivityService.java
index e3d4c45..c6f4c20 100644
--- a/services/java/com/android/server/ConnectivityService.java
+++ b/services/java/com/android/server/ConnectivityService.java
@@ -48,6 +48,7 @@
 import android.net.vpn.VpnManager;
 import android.net.wifi.WifiStateTracker;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.FileUtils;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -55,6 +56,7 @@
 import android.os.INetworkManagementService;
 import android.os.Looper;
 import android.os.Message;
+import android.os.ParcelFileDescriptor;
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -67,6 +69,8 @@
 
 import com.android.internal.telephony.Phone;
 import com.android.server.connectivity.Tethering;
+import com.android.server.connectivity.Vpn;
+
 import com.google.android.collect.Lists;
 
 import java.io.FileDescriptor;
@@ -103,6 +107,8 @@
     private Tethering mTethering;
     private boolean mTetheringConfigValid = false;
 
+    private Vpn mVpn;
+
     /** Currently active network rules by UID. */
     private SparseIntArray mUidRules = new SparseIntArray();
 
@@ -461,8 +467,11 @@
                                   mTethering.getTetherableBluetoothRegexs().length != 0) &&
                                  mTethering.getUpstreamIfaceRegexs().length != 0);
 
+        mVpn = new Vpn(mContext, new VpnCallback());
+
         try {
             nmService.registerObserver(mTethering);
+            nmService.registerObserver(mVpn);
         } catch (RemoteException e) {
             loge("Error registering observer :" + e);
         }
@@ -2358,6 +2367,7 @@
     private void loge(String s) {
         Slog.e(TAG, s);
     }
+
     int convertFeatureToNetworkType(String feature){
         int networkType = -1;
         if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_MMS)) {
@@ -2385,4 +2395,62 @@
         }
         return value;
     }
+
+    // @see ConnectivityManager#protectVpn(ParcelFileDescriptor)
+    // Permission checks are done in Vpn class.
+    @Override
+    public void protectVpn(ParcelFileDescriptor socket) {
+        mVpn.protect(socket, getDefaultInterface());
+    }
+
+    // @see ConnectivityManager#prepareVpn(String)
+    // Permission checks are done in Vpn class.
+    @Override
+    public String prepareVpn(String packageName) {
+        return mVpn.prepare(packageName);
+    }
+
+    // @see ConnectivityManager#establishVpn(Bundle)
+    // Permission checks are done in Vpn class.
+    @Override
+    public ParcelFileDescriptor establishVpn(Bundle config) {
+        return mVpn.establish(config);
+    }
+
+    private String getDefaultInterface() {
+        if (ConnectivityManager.isNetworkTypeValid(mActiveDefaultNetwork)) {
+            NetworkStateTracker tracker = mNetTrackers[mActiveDefaultNetwork];
+            if (tracker != null) {
+                LinkProperties properties = tracker.getLinkProperties();
+                if (properties != null) {
+                    return properties.getInterfaceName();
+                }
+            }
+        }
+        throw new IllegalStateException("No default interface");
+    }
+
+    /**
+     * Callback for VPN subsystem. Currently VPN is not adapted to the service
+     * through NetworkStateTracker since it works differently. For example, it
+     * needs to override DNS servers but never takes the default routes. It
+     * relies on another data network, and it could keep existing connections
+     * alive after reconnecting, switching between networks, or even resuming
+     * from deep sleep. Calls from applications should be done synchronously
+     * to avoid race conditions. As these are all hidden APIs, refactoring can
+     * be done whenever a better abstraction is developed.
+     */
+    public class VpnCallback {
+
+        private VpnCallback() {
+        }
+
+        public synchronized void override(String[] dnsServers) {
+            // TODO: override DNS servers and http proxy.
+        }
+
+        public synchronized void restore() {
+            // TODO: restore VPN changes.
+        }
+    }
 }
diff --git a/services/java/com/android/server/connectivity/Vpn.java b/services/java/com/android/server/connectivity/Vpn.java
new file mode 100644
index 0000000..b754dba
--- /dev/null
+++ b/services/java/com/android/server/connectivity/Vpn.java
@@ -0,0 +1,258 @@
+/*
+ * 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 android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.net.INetworkManagementEventObserver;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.R;
+import com.android.server.ConnectivityService.VpnCallback;
+
+/**
+ * @hide
+ */
+public class Vpn extends INetworkManagementEventObserver.Stub {
+
+    private final static String TAG = "Vpn";
+    private final static String VPN = android.Manifest.permission.VPN;
+
+    private final Context mContext;
+    private final VpnCallback mCallback;
+
+    private String mPackageName;
+    private String mInterfaceName;
+    private String mDnsPropertyPrefix;
+
+    public Vpn(Context context, VpnCallback callback) {
+        mContext = context;
+        mCallback = callback;
+    }
+
+    /**
+     * Prepare for a VPN application.
+     *
+     * @param packageName The package name of the new VPN application.
+     * @return The name of the current prepared package.
+     */
+    public synchronized String prepare(String packageName) {
+
+        // TODO: Check if the caller is VpnDialogs.
+
+        if (packageName == null) {
+            return mPackageName;
+        }
+
+        // Check the permission of the given application.
+        PackageManager pm = mContext.getPackageManager();
+        if (pm.checkPermission(VPN, packageName) != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException(packageName + " does not have " + VPN);
+        }
+
+        // Reset the interface and hide the notification.
+        if (mInterfaceName != null) {
+            nativeReset(mInterfaceName);
+            mInterfaceName = null;
+            hideNotification();
+            // TODO: Send out a broadcast.
+        }
+
+        mPackageName = packageName;
+        Log.i(TAG, "Prepared for " + packageName);
+        return mPackageName;
+    }
+
+    /**
+     * 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 name The name of the interface.
+     */
+    public void protect(ParcelFileDescriptor socket, String name) {
+        mContext.enforceCallingPermission(VPN, "protect");
+        nativeProtect(socket.getFd(), name);
+    }
+
+    /**
+     * Configure a TUN interface and return its file descriptor.
+     *
+     * @param configuration The parameters to configure the interface.
+     * @return The file descriptor of the interface.
+     */
+    public synchronized ParcelFileDescriptor establish(Bundle config) {
+        // Check the permission of the caller.
+        mContext.enforceCallingPermission(VPN, "establish");
+
+        // Check if the caller is already prepared.
+        PackageManager pm = mContext.getPackageManager();
+        ApplicationInfo app = null;
+        try {
+            app = pm.getApplicationInfo(mPackageName, 0);
+        } catch (Exception e) {
+            throw new SecurityException("Not prepared");
+        }
+        if (Binder.getCallingUid() != app.uid) {
+            throw new SecurityException("Not prepared");
+        }
+
+        // Unpack the config.
+        // TODO: move constants into VpnBuilder.
+        String session = config.getString("session");
+        String addresses = config.getString("addresses");
+        String routes = config.getString("routes");
+        String dnsServers = config.getString("dnsServers");
+
+        // Create interface and configure addresses and routes.
+        ParcelFileDescriptor descriptor = nativeConfigure(addresses, routes);
+
+        // Replace the interface and abort if it fails.
+        try {
+            String interfaceName = nativeGetName(descriptor.getFd());
+
+            if (mInterfaceName != null && !mInterfaceName.equals(interfaceName)) {
+                nativeReset(mInterfaceName);
+            }
+            mInterfaceName = interfaceName;
+        } catch (RuntimeException e) {
+            try {
+                descriptor.close();
+            } catch (Exception ex) {
+                // ignore
+            }
+            throw e;
+        }
+
+        dnsServers = (dnsServers == null) ? "" : dnsServers.trim();
+        mCallback.override(dnsServers.isEmpty() ? null : dnsServers.split(" "));
+
+        showNotification(pm, app, session);
+        return descriptor;
+    }
+
+    public synchronized boolean onInterfaceRemoved(String name) {
+        if (name.equals(mInterfaceName) && nativeCheck(name) == 0) {
+            hideNotification();
+            mInterfaceName = null;
+            return true;
+        }
+        return false;
+    }
+
+    // INetworkManagementEventObserver.Stub
+    public void interfaceLinkStatusChanged(String name, boolean up) {
+    }
+
+    // INetworkManagementEventObserver.Stub
+    public void interfaceAdded(String name) {
+    }
+
+    // INetworkManagementEventObserver.Stub
+    public synchronized void interfaceRemoved(String name) {
+        if (name.equals(mInterfaceName) && nativeCheck(name) == 0) {
+            hideNotification();
+            mInterfaceName = null;
+            mCallback.restore();
+        }
+    }
+
+    private void showNotification(PackageManager pm, ApplicationInfo app, String session) {
+        NotificationManager nm = (NotificationManager)
+                mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+
+        if (nm != null) {
+            // 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);
+                icon.draw(new Canvas(bitmap));
+            }
+
+            // Load the label.
+            String label = app.loadLabel(pm).toString();
+
+            // If session is null, use the application name instead.
+            if (session == null) {
+                session = label;
+            }
+
+            // Build the intent.
+            // TODO: move these into VpnBuilder.
+            Intent intent = new Intent();
+            intent.setClassName("com.android.vpndialogs",
+                    "com.android.vpndialogs.ManageDialog");
+            intent.putExtra("packageName", mPackageName);
+            intent.putExtra("interfaceName", mInterfaceName);
+            intent.putExtra("session", session);
+            intent.putExtra("startTime", android.os.SystemClock.elapsedRealtime());
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+
+            // Build the notification.
+            long identity = Binder.clearCallingIdentity();
+            Notification notification = new Notification.Builder(mContext)
+                    .setSmallIcon(R.drawable.vpn_connected)
+                    .setLargeIcon(bitmap)
+                    .setTicker(mContext.getString(R.string.vpn_ticker, label))
+                    .setContentTitle(mContext.getString(R.string.vpn_title, label))
+                    .setContentText(mContext.getString(R.string.vpn_text, session))
+                    .setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0))
+                    .setDefaults(Notification.DEFAULT_ALL)
+                    .setOngoing(true)
+                    .getNotification();
+
+            nm.notify(R.drawable.vpn_connected, notification);
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    private void hideNotification() {
+        NotificationManager nm = (NotificationManager)
+                mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+
+        if (nm != null) {
+            long identity = Binder.clearCallingIdentity();
+            nm.cancel(R.drawable.vpn_connected);
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    private native ParcelFileDescriptor nativeConfigure(String addresses, String routes);
+    private native String nativeGetName(int fd);
+    private native void nativeReset(String name);
+    private native int nativeCheck(String name);
+    private native void nativeProtect(int fd, String name);
+}
diff --git a/services/jni/Android.mk b/services/jni/Android.mk
index a1c3283..f33920d 100644
--- a/services/jni/Android.mk
+++ b/services/jni/Android.mk
@@ -16,6 +16,7 @@
     com_android_server_UsbHostManager.cpp \
     com_android_server_VibratorService.cpp \
     com_android_server_location_GpsLocationProvider.cpp \
+    com_android_server_connectivity_Vpn.cpp \
     onload.cpp
 
 LOCAL_C_INCLUDES += \
diff --git a/services/jni/com_android_server_connectivity_Vpn.cpp b/services/jni/com_android_server_connectivity_Vpn.cpp
new file mode 100644
index 0000000..374fd3b
--- /dev/null
+++ b/services/jni/com_android_server_connectivity_Vpn.cpp
@@ -0,0 +1,450 @@
+/*
+ * 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.
+ */
+
+#define LOG_NDEBUG 0
+
+#define LOG_TAG "VpnJni"
+#include <cutils/log.h>
+#include <cutils/properties.h>
+
+#include <stdio.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+
+#include <linux/if.h>
+#include <linux/if_tun.h>
+#include <linux/route.h>
+#include <linux/ipv6_route.h>
+
+#include "jni.h"
+#include "JNIHelp.h"
+#include "android_util_Binder.h"
+
+namespace android
+{
+
+static inline void init_sockaddr(sockaddr *sa) {
+    ((sockaddr_in *)sa)->sin_family = AF_INET;
+    ((sockaddr_in *)sa)->sin_port = 0;
+}
+
+static inline in_addr_t *as_in_addr(sockaddr *sa) {
+    return &((sockaddr_in *)sa)->sin_addr.s_addr;
+}
+
+static inline in_addr_t *as_in_addr(sockaddr_storage *ss) {
+    return &((sockaddr_in *)ss)->sin_addr.s_addr;
+}
+
+static inline in6_addr *as_in6_addr(sockaddr_storage *ss) {
+    return &((sockaddr_in6 *)&ss)->sin6_addr;
+}
+
+//------------------------------------------------------------------------------
+
+#define SYSTEM_ERROR -1
+#define BAD_ARGUMENT -2
+
+static int create_interface(char *name, int *index)
+{
+    int tun = open("/dev/tun", O_RDWR);
+    int inet4 = socket(AF_INET, SOCK_DGRAM, 0);
+
+    ifreq ifr4;
+    memset(&ifr4, 0, sizeof(ifr4));
+
+    // Allocate interface.
+    ifr4.ifr_flags = IFF_TUN;
+    if (ioctl(tun, TUNSETIFF, &ifr4)) {
+        LOGE("Cannot allocate TUN: %s", strerror(errno));
+        goto error;
+    }
+
+    // Activate interface.
+    ifr4.ifr_flags = IFF_UP;
+    if (ioctl(inet4, SIOCSIFFLAGS, &ifr4)) {
+        LOGE("Cannot activate %s: %s", ifr4.ifr_name, strerror(errno));
+        goto error;
+    }
+
+    // Get interface index.
+    if (ioctl(inet4, SIOGIFINDEX, &ifr4)) {
+        LOGE("Cannot get index of %s: %s", ifr4.ifr_name, strerror(errno));
+        goto error;
+    }
+
+    strcpy(name, ifr4.ifr_name);
+    *index = ifr4.ifr_ifindex;
+    close(inet4);
+    return tun;
+
+error:
+    close(tun);
+    close(inet4);
+    return SYSTEM_ERROR;
+}
+
+static int set_addresses(const char *name, int index, const char *addresses)
+{
+    int inet4 = socket(AF_INET, SOCK_DGRAM, 0);
+    int inet6 = socket(AF_INET6, SOCK_DGRAM, 0);
+
+    ifreq ifr4;
+    memset(&ifr4, 0, sizeof(ifr4));
+    strcpy(ifr4.ifr_name, name);
+    init_sockaddr(&ifr4.ifr_addr);
+
+    in6_ifreq ifr6;
+    memset(&ifr6, 0, sizeof(ifr6));
+    ifr6.ifr6_ifindex = index;
+
+    char address[65];
+    int prefix;
+
+    int chars;
+    int count = 0;
+
+    while (sscanf(addresses, " %64[^/]/%d %n", address, &prefix, &chars) == 2) {
+        addresses += chars;
+
+        if (strchr(address, ':')) {
+            // Add an IPv6 address.
+            if (inet_pton(AF_INET6, address, &ifr6.ifr6_addr) != 1 ||
+                    prefix < 0 || prefix > 128) {
+                count = BAD_ARGUMENT;
+                break;
+            }
+
+            ifr6.ifr6_prefixlen = prefix;
+            if (ioctl(inet6, SIOCSIFADDR, &ifr6)) {
+                count = (errno == EINVAL) ? BAD_ARGUMENT : SYSTEM_ERROR;
+                break;
+            }
+        } else {
+            // Add an IPv4 address.
+            if (inet_pton(AF_INET, address, as_in_addr(&ifr4.ifr_addr)) != 1 ||
+                    prefix < 0 || prefix > 32) {
+                count = BAD_ARGUMENT;
+                break;
+            }
+
+            if (count) {
+                sprintf(ifr4.ifr_name, "%s:%d", name, count);
+            }
+            if (ioctl(inet4, SIOCSIFADDR, &ifr4)) {
+                count = (errno == EINVAL) ? BAD_ARGUMENT : SYSTEM_ERROR;
+                break;
+            }
+
+            in_addr_t mask = prefix ? (~0 << (32 - prefix)) : 0;
+            *as_in_addr(&ifr4.ifr_addr) = htonl(mask);
+            if (ioctl(inet4, SIOCSIFNETMASK, &ifr4)) {
+                count = (errno == EINVAL) ? BAD_ARGUMENT : SYSTEM_ERROR;
+                break;
+            }
+        }
+        LOGV("Address added on %s: %s/%d", name, address, prefix);
+        ++count;
+    }
+
+    if (count == BAD_ARGUMENT) {
+        LOGE("Invalid address: %s/%d", address, prefix);
+    } else if (count == SYSTEM_ERROR) {
+        LOGE("Cannot add address: %s/%d: %s", address, prefix, strerror(errno));
+    } else if (*addresses) {
+        LOGE("Invalid address: %s", addresses);
+        count = BAD_ARGUMENT;
+    }
+
+    close(inet4);
+    close(inet6);
+    return count;
+}
+
+static int set_routes(const char *name, int index, const char *routes)
+{
+    int inet4 = socket(AF_INET, SOCK_DGRAM, 0);
+    int inet6 = socket(AF_INET6, SOCK_DGRAM, 0);
+
+    rtentry rt4;
+    memset(&rt4, 0, sizeof(rt4));
+    rt4.rt_dev = (char *)name;
+    rt4.rt_flags = RTF_UP;
+    init_sockaddr(&rt4.rt_dst);
+    init_sockaddr(&rt4.rt_genmask);
+    init_sockaddr(&rt4.rt_gateway);
+
+    in6_rtmsg rt6;
+    memset(&rt6, 0, sizeof(rt6));
+    rt6.rtmsg_ifindex = index;
+    rt6.rtmsg_flags = RTF_UP;
+
+    char address[65];
+    int prefix;
+    char gateway[65];
+
+    int chars;
+    int count = 0;
+
+    while (sscanf(routes, " %64[^/]/%d>%64[^ ] %n",
+            address, &prefix, gateway, &chars) == 3) {
+        routes += chars;
+
+        if (strchr(address, ':')) {
+            // Add an IPv6 route.
+            if (inet_pton(AF_INET6, gateway, &rt6.rtmsg_gateway) != 1 ||
+                    inet_pton(AF_INET6, address, &rt6.rtmsg_dst) != 1 ||
+                    prefix < 0 || prefix > 128) {
+                count = BAD_ARGUMENT;
+                break;
+            }
+
+            rt6.rtmsg_dst_len = prefix;
+            if (memcmp(&rt6.rtmsg_gateway, &in6addr_any, sizeof(in6addr_any))) {
+                rt6.rtmsg_flags |= RTF_GATEWAY;
+            }
+            if (ioctl(inet6, SIOCADDRT, &rt6)) {
+                count = (errno == EINVAL) ? BAD_ARGUMENT : SYSTEM_ERROR;
+                break;
+            }
+        } else {
+            // Add an IPv4 route.
+            if (inet_pton(AF_INET, gateway, as_in_addr(&rt4.rt_gateway)) != 1 ||
+                    inet_pton(AF_INET, address, as_in_addr(&rt4.rt_dst)) != 1 ||
+                    prefix < 0 || prefix > 32) {
+                count = BAD_ARGUMENT;
+                break;
+            }
+
+            in_addr_t mask = prefix ? (~0 << (32 - prefix)) : 0;
+            *as_in_addr(&rt4.rt_genmask) = htonl(mask);
+            if (*as_in_addr(&rt4.rt_gateway)) {
+                rt4.rt_flags |= RTF_GATEWAY;
+            }
+            if (ioctl(inet4, SIOCADDRT, &rt4)) {
+                count = (errno == EINVAL) ? BAD_ARGUMENT : SYSTEM_ERROR;
+                break;
+            }
+        }
+        LOGV("Route added on %s: %s/%d -> %s", name, address, prefix, gateway);
+        ++count;
+    }
+
+    if (count == BAD_ARGUMENT) {
+        LOGE("Invalid route: %s/%d -> %s", address, prefix, gateway);
+    } else if (count == SYSTEM_ERROR) {
+        LOGE("Cannot add route: %s/%d -> %s: %s",
+                address, prefix, gateway, strerror(errno));
+    } else if (*routes) {
+        LOGE("Invalid route: %s", routes);
+        count = BAD_ARGUMENT;
+    }
+
+    close(inet4);
+    close(inet6);
+    return count;
+}
+
+static int get_interface_name(char *name, int tun)
+{
+    ifreq ifr4;
+    if (ioctl(tun, TUNGETIFF, &ifr4)) {
+        LOGE("Cannot get interface name: %s", strerror(errno));
+        return SYSTEM_ERROR;
+    }
+    strcpy(name, ifr4.ifr_name);
+    return 0;
+}
+
+static int reset_interface(const char *name)
+{
+    int inet4 = socket(AF_INET, SOCK_DGRAM, 0);
+
+    ifreq ifr4;
+    ifr4.ifr_flags = 0;
+    strncpy(ifr4.ifr_name, name, IFNAMSIZ);
+
+    if (ioctl(inet4, SIOCSIFFLAGS, &ifr4) && errno != ENODEV) {
+        LOGE("Cannot reset %s: %s", name, strerror(errno));
+        close(inet4);
+        return SYSTEM_ERROR;
+    }
+    close(inet4);
+    return 0;
+}
+
+static int check_interface(const char *name)
+{
+    int inet4 = socket(AF_INET, SOCK_DGRAM, 0);
+
+    ifreq ifr4;
+    strncpy(ifr4.ifr_name, name, IFNAMSIZ);
+
+    if (ioctl(inet4, SIOCGIFFLAGS, &ifr4) && errno != ENODEV) {
+        LOGE("Cannot check %s: %s", name, strerror(errno));
+        ifr4.ifr_flags = 0;
+    }
+    close(inet4);
+    return ifr4.ifr_flags;
+}
+
+static int bind_to_interface(int fd, const char *name)
+{
+    if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, name, strlen(name))) {
+        LOGE("Cannot bind socket to %s: %s", name, strerror(errno));
+        return SYSTEM_ERROR;
+    }
+    return 0;
+}
+
+//------------------------------------------------------------------------------
+
+static void throwException(JNIEnv *env, int error, const char *message)
+{
+    if (error == SYSTEM_ERROR) {
+        jniThrowException(env, "java/lang/IllegalStateException", message);
+    } else {
+        jniThrowException(env, "java/lang/IllegalArgumentException", message);
+    }
+}
+
+static jobject configure(JNIEnv *env, jobject thiz,
+        jstring jAddresses, jstring jRoutes)
+{
+    char name[IFNAMSIZ];
+    int index;
+    int tun = create_interface(name, &index);
+    if (tun < 0) {
+        throwException(env, tun, "Cannot create interface");
+        return NULL;
+    }
+    LOGD("%s is created", name);
+
+    const char *addresses;
+    const char *routes;
+    int count;
+
+    // Addresses are required.
+    addresses = jAddresses ? env->GetStringUTFChars(jAddresses, NULL) : NULL;
+    if (!addresses) {
+        jniThrowNullPointerException(env, "address");
+        goto error;
+    }
+    count = set_addresses(name, index, addresses);
+    env->ReleaseStringUTFChars(jAddresses, addresses);
+    if (count <= 0) {
+        throwException(env, count, "Cannot set address");
+        goto error;
+    }
+    LOGD("Configured %d address(es) on %s", count, name);
+
+    // Routes are optional.
+    routes = jRoutes ? env->GetStringUTFChars(jRoutes, NULL) : NULL;
+    if (routes) {
+        count = set_routes(name, index, routes);
+        env->ReleaseStringUTFChars(jRoutes, routes);
+        if (count < 0) {
+            throwException(env, count, "Cannot set route");
+            goto error;
+        }
+        LOGD("Configured %d route(s) on %s", count, name);
+    }
+
+    return newParcelFileDescriptor(env, jniCreateFileDescriptor(env, tun));
+
+error:
+    close(tun);
+    LOGD("%s is destroyed", name);
+    return NULL;
+}
+
+static jstring getName(JNIEnv *env, jobject thiz, jint fd)
+{
+    char name[IFNAMSIZ];
+    if (get_interface_name(name, fd) < 0) {
+        throwException(env, SYSTEM_ERROR, "Cannot get interface name");
+        return NULL;
+    }
+    return env->NewStringUTF(name);
+}
+
+static void reset(JNIEnv *env, jobject thiz, jstring jName)
+{
+    const char *name = jName ?
+            env->GetStringUTFChars(jName, NULL) : NULL;
+    if (!name) {
+        jniThrowNullPointerException(env, "name");
+        return;
+    }
+    if (reset_interface(name) < 0) {
+        throwException(env, SYSTEM_ERROR, "Cannot reset interface");
+    } else {
+        LOGD("%s is deactivated", name);
+    }
+    env->ReleaseStringUTFChars(jName, name);
+}
+
+static jint check(JNIEnv *env, jobject thiz, jstring jName)
+{
+    const char *name = jName ?
+            env->GetStringUTFChars(jName, NULL) : NULL;
+    if (!name) {
+        jniThrowNullPointerException(env, "name");
+        return 0;
+    }
+    int flags = check_interface(name);
+    env->ReleaseStringUTFChars(jName, name);
+    return flags;
+}
+
+static void protect(JNIEnv *env, jobject thiz, jint fd, jstring jName)
+{
+    const char *name = jName ?
+            env->GetStringUTFChars(jName, NULL) : NULL;
+    if (!name) {
+        jniThrowNullPointerException(env, "name");
+        return;
+    }
+    if (bind_to_interface(fd, name) < 0) {
+        throwException(env, SYSTEM_ERROR, "Cannot protect socket");
+    }
+    env->ReleaseStringUTFChars(jName, name);
+}
+
+//------------------------------------------------------------------------------
+
+static JNINativeMethod gMethods[] = {
+    {"nativeConfigure", "(Ljava/lang/String;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor;", (void *)configure},
+    {"nativeGetName", "(I)Ljava/lang/String;", (void *)getName},
+    {"nativeReset", "(Ljava/lang/String;)V", (void *)reset},
+    {"nativeCheck", "(Ljava/lang/String;)I", (void *)check},
+    {"nativeProtect", "(ILjava/lang/String;)V", (void *)protect},
+};
+
+int register_android_server_connectivity_Vpn(JNIEnv *env)
+{
+    return jniRegisterNativeMethods(env, "com/android/server/connectivity/Vpn",
+            gMethods, NELEM(gMethods));
+}
+
+};
diff --git a/services/jni/onload.cpp b/services/jni/onload.cpp
index 469e818..9dff48b 100644
--- a/services/jni/onload.cpp
+++ b/services/jni/onload.cpp
@@ -34,6 +34,7 @@
 int register_android_server_VibratorService(JNIEnv* env);
 int register_android_server_SystemServer(JNIEnv* env);
 int register_android_server_location_GpsLocationProvider(JNIEnv* env);
+int register_android_server_connectivity_Vpn(JNIEnv* env);
 };
 
 using namespace android;
@@ -63,6 +64,7 @@
     register_android_server_VibratorService(env);
     register_android_server_SystemServer(env);
     register_android_server_location_GpsLocationProvider(env);
+    register_android_server_connectivity_Vpn(env);
 
     return JNI_VERSION_1_4;
 }