From ff3bdca31f4cf2bd607519b276dd175763aa1784 Mon Sep 17 00:00:00 2001 From: Chia-chi Yeh Date: Mon, 23 May 2011 17:26:46 -0700 Subject: The service part of the user space VPN support. The dialogs will be in another change. Change-Id: I0cdfd2ef21ffd40ee955b3cbde5ada65dbfdb0bc --- core/java/android/net/ConnectivityManager.java | 41 ++ core/java/android/net/IConnectivityManager.aidl | 8 + core/res/res/values/strings.xml | 9 +- .../com/android/server/ConnectivityService.java | 68 ++++ .../java/com/android/server/connectivity/Vpn.java | 258 ++++++++++++ services/jni/Android.mk | 1 + .../jni/com_android_server_connectivity_Vpn.cpp | 450 +++++++++++++++++++++ services/jni/onload.cpp | 2 + 8 files changed, 836 insertions(+), 1 deletion(-) create mode 100644 services/java/com/android/server/connectivity/Vpn.java create mode 100644 services/jni/com_android_server_connectivity_Vpn.cpp diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index ea750da..3025462 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -19,6 +19,8 @@ package android.net; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.os.Binder; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import java.net.InetAddress; @@ -756,4 +758,43 @@ public class ConnectivityManager { } catch (RemoteException e) { } } + + /** + * Protect a socket from routing changes. This method is limited to VPN + * applications, and it is always hidden to avoid direct use. + * @hide + */ + public void protectVpn(ParcelFileDescriptor socket) { + try { + mService.protectVpn(socket); + } catch (RemoteException e) { + } + } + + /** + * Prepare for a VPN application. This method is limited to VpnDialogs, + * and it is always hidden to avoid direct use. + * @hide + */ + public String prepareVpn(String packageName) { + try { + return mService.prepareVpn(packageName); + } catch (RemoteException e) { + return null; + } + } + + /** + * Configure a TUN interface and return its file descriptor. Parameters + * are encoded and opaque to this class. This method is limited to VPN + * applications, and it is always hidden to avoid direct use. + * @hide + */ + public ParcelFileDescriptor establishVpn(Bundle config) { + try { + return mService.establishVpn(config); + } catch (RemoteException e) { + return null; + } + } } diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index 07f6cec..7f3775d 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -20,7 +20,9 @@ import android.net.LinkProperties; import android.net.NetworkInfo; import android.net.NetworkState; import android.net.ProxyProperties; +import android.os.Bundle; import android.os.IBinder; +import android.os.ParcelFileDescriptor; /** * Interface that answers queries about, and allows changing, the @@ -95,4 +97,10 @@ interface IConnectivityManager ProxyProperties getProxy(); void setDataDependency(int networkType, boolean met); + + void protectVpn(in ParcelFileDescriptor socket); + + String prepareVpn(String packageName); + + ParcelFileDescriptor establishVpn(in Bundle config); } diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 65a1e44..87d5654 100755 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1330,7 +1330,7 @@ intercept and modify all network traffic Allows an application to intercept and - inspect all network traffic, for example to establish a VPN connection. + inspect all network traffic to establish a VPN connection. Malicious applications may monitor, redirect, or modify network packets without your knowledge. @@ -2725,6 +2725,13 @@ Pre-shared key based L2TP/IPSec VPN Certificate based L2TP/IPSec VPN + + Activating %s VPN... + + %s VPN is active + + VPN is connected to %s. Tap to manage the network. + Choose file 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.RouteInfo; 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.IBinder; 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 android.util.SparseIntArray; 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 @@ public class ConnectivityService extends IConnectivityManager.Stub { 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 @@ public class ConnectivityService extends IConnectivityManager.Stub { 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 @@ public class ConnectivityService extends IConnectivityManager.Stub { 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 @@ public class ConnectivityService extends IConnectivityManager.Stub { } 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 @@ LOCAL_SRC_FILES:= \ 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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#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_UsbHostManager(JNIEnv* env); 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 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) 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; } -- cgit v1.1