summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHung-ying Tyan <tyanh@google.com>2011-01-19 16:48:38 +0800
committerHung-ying Tyan <tyanh@google.com>2011-01-20 12:51:43 +0800
commit52261a55efd313240b6fcae21b242385cf99f8b0 (patch)
tree2ee9d0d2df8a2985ac4d45b3df47990a6afdf4f4
parentc14915a2cebeab5998be2c65da69af2faa0a6c86 (diff)
downloadframeworks_native-52261a55efd313240b6fcae21b242385cf99f8b0.zip
frameworks_native-52261a55efd313240b6fcae21b242385cf99f8b0.tar.gz
frameworks_native-52261a55efd313240b6fcae21b242385cf99f8b0.tar.bz2
Make VpnService synchronous API.
This eases VpnSettings on dealing with multiple-activity-instance problem (i.e., SettingsActivity and VpnSettingsActivity). + Most of the code is moved from the VpnServices package to vpn/java/. + VpnManager and VpnServiceBinder are revised to provide synchronous API. + Add a new method isIdle() to IVpnService.aidl. Related bug: 3293236 (need to deal with multiple-activity-instance problem) Change-Id: I03afa3b3af85d7b4ef800683cd075c356a9266c4
-rw-r--r--vpn/java/android/net/vpn/IVpnService.aidl13
-rw-r--r--vpn/java/android/net/vpn/VpnManager.java106
-rw-r--r--vpn/java/com/android/server/vpn/DaemonProxy.java199
-rw-r--r--vpn/java/com/android/server/vpn/L2tpIpsecPskService.java47
-rw-r--r--vpn/java/com/android/server/vpn/L2tpIpsecService.java50
-rw-r--r--vpn/java/com/android/server/vpn/L2tpService.java35
-rw-r--r--vpn/java/com/android/server/vpn/PptpService.java34
-rw-r--r--vpn/java/com/android/server/vpn/VpnConnectingError.java35
-rw-r--r--vpn/java/com/android/server/vpn/VpnDaemons.java147
-rw-r--r--vpn/java/com/android/server/vpn/VpnService.java477
-rw-r--r--vpn/java/com/android/server/vpn/VpnServiceBinder.java117
11 files changed, 1223 insertions, 37 deletions
diff --git a/vpn/java/android/net/vpn/IVpnService.aidl b/vpn/java/android/net/vpn/IVpnService.aidl
index fedccb0..6bf3edd 100644
--- a/vpn/java/android/net/vpn/IVpnService.aidl
+++ b/vpn/java/android/net/vpn/IVpnService.aidl
@@ -24,10 +24,11 @@ import android.net.vpn.VpnProfile;
*/
interface IVpnService {
/**
- * Sets up the VPN connection.
+ * Sets up a VPN connection.
* @param profile the profile object
* @param username the username for authentication
* @param password the corresponding password for authentication
+ * @return true if VPN is successfully connected
*/
boolean connect(in VpnProfile profile, String username, String password);
@@ -37,7 +38,13 @@ interface IVpnService {
void disconnect();
/**
- * Makes the service broadcast the connectivity state.
+ * Gets the the current connection state.
*/
- void checkStatus(in VpnProfile profile);
+ String getState(in VpnProfile profile);
+
+ /**
+ * Returns the idle state.
+ * @return true if the system is not connecting/connected to a VPN
+ */
+ boolean isIdle();
}
diff --git a/vpn/java/android/net/vpn/VpnManager.java b/vpn/java/android/net/vpn/VpnManager.java
index ce40b5d..02486bb 100644
--- a/vpn/java/android/net/vpn/VpnManager.java
+++ b/vpn/java/android/net/vpn/VpnManager.java
@@ -16,17 +16,19 @@
package android.net.vpn;
-import java.io.File;
-
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.ServiceConnection;
import android.os.Environment;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.SystemProperties;
import android.util.Log;
+import com.android.server.vpn.VpnServiceBinder;
+
/**
* The class provides interface to manage all VPN-related tasks, including:
* <ul>
@@ -40,8 +42,6 @@ import android.util.Log;
* {@hide}
*/
public class VpnManager {
- // Action for broadcasting a connectivity state.
- private static final String ACTION_VPN_CONNECTIVITY = "vpn.connectivity";
/** Key to the profile name of a connectivity broadcast event. */
public static final String BROADCAST_PROFILE_NAME = "profile_name";
/** Key to the connectivity state of a connectivity broadcast event. */
@@ -74,8 +74,10 @@ public class VpnManager {
private static final String PACKAGE_PREFIX =
VpnManager.class.getPackage().getName() + ".";
- // Action to start VPN service
- private static final String ACTION_VPN_SERVICE = PACKAGE_PREFIX + "SERVICE";
+ // Action for broadcasting a connectivity state.
+ private static final String ACTION_VPN_CONNECTIVITY = "vpn.connectivity";
+
+ private static final String VPN_SERVICE_NAME = "vpn";
// Action to start VPN settings
private static final String ACTION_VPN_SETTINGS =
@@ -96,13 +98,76 @@ public class VpnManager {
return VpnType.values();
}
+ public static void startVpnService(Context c) {
+ ServiceManager.addService(VPN_SERVICE_NAME, new VpnServiceBinder(c));
+ }
+
private Context mContext;
+ private IVpnService mVpnService;
/**
* Creates a manager object with the specified context.
*/
public VpnManager(Context c) {
mContext = c;
+ createVpnServiceClient();
+ }
+
+ private void createVpnServiceClient() {
+ IBinder b = ServiceManager.getService(VPN_SERVICE_NAME);
+ mVpnService = IVpnService.Stub.asInterface(b);
+ }
+
+ /**
+ * Sets up a VPN connection.
+ * @param profile the profile object
+ * @param username the username for authentication
+ * @param password the corresponding password for authentication
+ * @return true if VPN is successfully connected
+ */
+ public boolean connect(VpnProfile p, String username, String password) {
+ try {
+ return mVpnService.connect(p, username, password);
+ } catch (RemoteException e) {
+ Log.e(TAG, "connect()", e);
+ return false;
+ }
+ }
+
+ /**
+ * Tears down the VPN connection.
+ */
+ public void disconnect() {
+ try {
+ mVpnService.disconnect();
+ } catch (RemoteException e) {
+ Log.e(TAG, "disconnect()", e);
+ }
+ }
+
+ /**
+ * Gets the the current connection state.
+ */
+ public VpnState getState(VpnProfile p) {
+ try {
+ return Enum.valueOf(VpnState.class, mVpnService.getState(p));
+ } catch (RemoteException e) {
+ Log.e(TAG, "getState()", e);
+ return VpnState.IDLE;
+ }
+ }
+
+ /**
+ * Returns the idle state.
+ * @return true if the system is not connecting/connected to a VPN
+ */
+ public boolean isIdle() {
+ try {
+ return mVpnService.isIdle();
+ } catch (RemoteException e) {
+ Log.e(TAG, "isIdle()", e);
+ return true;
+ }
}
/**
@@ -134,33 +199,6 @@ public class VpnManager {
}
}
- /**
- * Starts the VPN service to establish VPN connection.
- */
- public void startVpnService() {
- mContext.startService(new Intent(ACTION_VPN_SERVICE));
- }
-
- /**
- * Stops the VPN service.
- */
- public void stopVpnService() {
- mContext.stopService(new Intent(ACTION_VPN_SERVICE));
- }
-
- /**
- * Binds the specified ServiceConnection with the VPN service.
- */
- public boolean bindVpnService(ServiceConnection c) {
- if (!mContext.bindService(new Intent(ACTION_VPN_SERVICE), c, 0)) {
- Log.w(TAG, "failed to connect to VPN service");
- return false;
- } else {
- Log.d(TAG, "succeeded to connect to VPN service");
- return true;
- }
- }
-
/** Broadcasts the connectivity state of the specified profile. */
public void broadcastConnectivity(String profileName, VpnState s) {
broadcastConnectivity(profileName, s, VPN_ERROR_NO_ERROR);
diff --git a/vpn/java/com/android/server/vpn/DaemonProxy.java b/vpn/java/com/android/server/vpn/DaemonProxy.java
new file mode 100644
index 0000000..289ee45
--- /dev/null
+++ b/vpn/java/com/android/server/vpn/DaemonProxy.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2009, 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.vpn;
+
+import android.net.LocalSocket;
+import android.net.LocalSocketAddress;
+import android.net.vpn.VpnManager;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+
+/**
+ * Proxy to start, stop and interact with a VPN daemon.
+ * The daemon is expected to accept connection through Unix domain socket.
+ * When the proxy successfully starts the daemon, it will establish a socket
+ * connection with the daemon, to both send commands to the daemon and receive
+ * response and connecting error code from the daemon.
+ */
+class DaemonProxy implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final boolean DBG = true;
+
+ private static final int WAITING_TIME = 15; // sec
+
+ private static final String SVC_STATE_CMD_PREFIX = "init.svc.";
+ private static final String SVC_START_CMD = "ctl.start";
+ private static final String SVC_STOP_CMD = "ctl.stop";
+ private static final String SVC_STATE_RUNNING = "running";
+ private static final String SVC_STATE_STOPPED = "stopped";
+
+ private static final int END_OF_ARGUMENTS = 255;
+
+ private String mName;
+ private String mTag;
+ private transient LocalSocket mControlSocket;
+
+ /**
+ * Creates a proxy of the specified daemon.
+ * @param daemonName name of the daemon
+ */
+ DaemonProxy(String daemonName) {
+ mName = daemonName;
+ mTag = "SProxy_" + daemonName;
+ }
+
+ String getName() {
+ return mName;
+ }
+
+ void start() throws IOException {
+ String svc = mName;
+
+ Log.i(mTag, "Start VPN daemon: " + svc);
+ SystemProperties.set(SVC_START_CMD, svc);
+
+ if (!blockUntil(SVC_STATE_RUNNING, WAITING_TIME)) {
+ throw new IOException("cannot start service: " + svc);
+ } else {
+ mControlSocket = createServiceSocket();
+ }
+ }
+
+ void sendCommand(String ...args) throws IOException {
+ OutputStream out = getControlSocketOutput();
+ for (String arg : args) outputString(out, arg);
+ out.write(END_OF_ARGUMENTS);
+ out.flush();
+
+ int result = getResultFromSocket(true);
+ if (result != args.length) {
+ throw new IOException("socket error, result from service: "
+ + result);
+ }
+ }
+
+ // returns 0 if nothing is in the receive buffer
+ int getResultFromSocket() throws IOException {
+ return getResultFromSocket(false);
+ }
+
+ void closeControlSocket() {
+ if (mControlSocket == null) return;
+ try {
+ mControlSocket.close();
+ } catch (IOException e) {
+ Log.w(mTag, "close control socket", e);
+ } finally {
+ mControlSocket = null;
+ }
+ }
+
+ void stop() {
+ String svc = mName;
+ Log.i(mTag, "Stop VPN daemon: " + svc);
+ SystemProperties.set(SVC_STOP_CMD, svc);
+ boolean success = blockUntil(SVC_STATE_STOPPED, 5);
+ if (DBG) Log.d(mTag, "stopping " + svc + ", success? " + success);
+ }
+
+ boolean isStopped() {
+ String cmd = SVC_STATE_CMD_PREFIX + mName;
+ return SVC_STATE_STOPPED.equals(SystemProperties.get(cmd));
+ }
+
+ private int getResultFromSocket(boolean blocking) throws IOException {
+ LocalSocket s = mControlSocket;
+ if (s == null) return 0;
+ InputStream in = s.getInputStream();
+ if (!blocking && in.available() == 0) return 0;
+
+ int data = in.read();
+ Log.i(mTag, "got data from control socket: " + data);
+
+ return data;
+ }
+
+ private LocalSocket createServiceSocket() throws IOException {
+ LocalSocket s = new LocalSocket();
+ LocalSocketAddress a = new LocalSocketAddress(mName,
+ LocalSocketAddress.Namespace.RESERVED);
+
+ // try a few times in case the service has not listen()ed
+ IOException excp = null;
+ for (int i = 0; i < 10; i++) {
+ try {
+ s.connect(a);
+ return s;
+ } catch (IOException e) {
+ if (DBG) Log.d(mTag, "service not yet listen()ing; try again");
+ excp = e;
+ sleep(500);
+ }
+ }
+ throw excp;
+ }
+
+ private OutputStream getControlSocketOutput() throws IOException {
+ if (mControlSocket != null) {
+ return mControlSocket.getOutputStream();
+ } else {
+ throw new IOException("no control socket available");
+ }
+ }
+
+ /**
+ * Waits for the process to be in the expected state. The method returns
+ * false if after the specified duration (in seconds), the process is still
+ * not in the expected state.
+ */
+ private boolean blockUntil(String expectedState, int waitTime) {
+ String cmd = SVC_STATE_CMD_PREFIX + mName;
+ int sleepTime = 200; // ms
+ int n = waitTime * 1000 / sleepTime;
+ for (int i = 0; i < n; i++) {
+ if (expectedState.equals(SystemProperties.get(cmd))) {
+ if (DBG) {
+ Log.d(mTag, mName + " is " + expectedState + " after "
+ + (i * sleepTime) + " msec");
+ }
+ break;
+ }
+ sleep(sleepTime);
+ }
+ return expectedState.equals(SystemProperties.get(cmd));
+ }
+
+ private void outputString(OutputStream out, String s) throws IOException {
+ byte[] bytes = s.getBytes();
+ out.write(bytes.length);
+ out.write(bytes);
+ out.flush();
+ }
+
+ private void sleep(int msec) {
+ try {
+ Thread.currentThread().sleep(msec);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/vpn/java/com/android/server/vpn/L2tpIpsecPskService.java b/vpn/java/com/android/server/vpn/L2tpIpsecPskService.java
new file mode 100644
index 0000000..50e0de1
--- /dev/null
+++ b/vpn/java/com/android/server/vpn/L2tpIpsecPskService.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2009, 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.vpn;
+
+import android.net.vpn.L2tpIpsecPskProfile;
+
+import java.io.IOException;
+
+/**
+ * The service that manages the preshared key based L2TP-over-IPSec VPN
+ * connection.
+ */
+class L2tpIpsecPskService extends VpnService<L2tpIpsecPskProfile> {
+ private static final String IPSEC = "racoon";
+
+ @Override
+ protected void connect(String serverIp, String username, String password)
+ throws IOException {
+ L2tpIpsecPskProfile p = getProfile();
+ VpnDaemons daemons = getDaemons();
+
+ // IPSEC
+ daemons.startIpsecForL2tp(serverIp, p.getPresharedKey())
+ .closeControlSocket();
+
+ sleep(2000); // 2 seconds
+
+ // L2TP
+ daemons.startL2tp(serverIp,
+ (p.isSecretEnabled() ? p.getSecretString() : null),
+ username, password);
+ }
+}
diff --git a/vpn/java/com/android/server/vpn/L2tpIpsecService.java b/vpn/java/com/android/server/vpn/L2tpIpsecService.java
new file mode 100644
index 0000000..663b0e8
--- /dev/null
+++ b/vpn/java/com/android/server/vpn/L2tpIpsecService.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2009, 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.vpn;
+
+import android.net.vpn.L2tpIpsecProfile;
+import android.security.Credentials;
+
+import java.io.IOException;
+
+/**
+ * The service that manages the certificate based L2TP-over-IPSec VPN connection.
+ */
+class L2tpIpsecService extends VpnService<L2tpIpsecProfile> {
+ private static final String IPSEC = "racoon";
+
+ @Override
+ protected void connect(String serverIp, String username, String password)
+ throws IOException {
+ L2tpIpsecProfile p = getProfile();
+ VpnDaemons daemons = getDaemons();
+
+ // IPSEC
+ DaemonProxy ipsec = daemons.startIpsecForL2tp(serverIp,
+ Credentials.USER_PRIVATE_KEY + p.getUserCertificate(),
+ Credentials.USER_CERTIFICATE + p.getUserCertificate(),
+ Credentials.CA_CERTIFICATE + p.getCaCertificate());
+ ipsec.closeControlSocket();
+
+ sleep(2000); // 2 seconds
+
+ // L2TP
+ daemons.startL2tp(serverIp,
+ (p.isSecretEnabled() ? p.getSecretString() : null),
+ username, password);
+ }
+}
diff --git a/vpn/java/com/android/server/vpn/L2tpService.java b/vpn/java/com/android/server/vpn/L2tpService.java
new file mode 100644
index 0000000..784a366
--- /dev/null
+++ b/vpn/java/com/android/server/vpn/L2tpService.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2009, 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.vpn;
+
+import android.net.vpn.L2tpProfile;
+
+import java.io.IOException;
+
+/**
+ * The service that manages the L2TP VPN connection.
+ */
+class L2tpService extends VpnService<L2tpProfile> {
+ @Override
+ protected void connect(String serverIp, String username, String password)
+ throws IOException {
+ L2tpProfile p = getProfile();
+ getDaemons().startL2tp(serverIp,
+ (p.isSecretEnabled() ? p.getSecretString() : null),
+ username, password);
+ }
+}
diff --git a/vpn/java/com/android/server/vpn/PptpService.java b/vpn/java/com/android/server/vpn/PptpService.java
new file mode 100644
index 0000000..de12710
--- /dev/null
+++ b/vpn/java/com/android/server/vpn/PptpService.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2009, 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.vpn;
+
+import android.net.vpn.PptpProfile;
+
+import java.io.IOException;
+
+/**
+ * The service that manages the PPTP VPN connection.
+ */
+class PptpService extends VpnService<PptpProfile> {
+ @Override
+ protected void connect(String serverIp, String username, String password)
+ throws IOException {
+ PptpProfile p = getProfile();
+ getDaemons().startPptp(serverIp, username, password,
+ p.isEncryptionEnabled());
+ }
+}
diff --git a/vpn/java/com/android/server/vpn/VpnConnectingError.java b/vpn/java/com/android/server/vpn/VpnConnectingError.java
new file mode 100644
index 0000000..3c4ec7d
--- /dev/null
+++ b/vpn/java/com/android/server/vpn/VpnConnectingError.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2009, 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.vpn;
+
+import java.io.IOException;
+
+/**
+ * Exception thrown when a connecting attempt fails.
+ */
+class VpnConnectingError extends IOException {
+ private int mErrorCode;
+
+ VpnConnectingError(int errorCode) {
+ super("Connecting error: " + errorCode);
+ mErrorCode = errorCode;
+ }
+
+ int getErrorCode() {
+ return mErrorCode;
+ }
+}
diff --git a/vpn/java/com/android/server/vpn/VpnDaemons.java b/vpn/java/com/android/server/vpn/VpnDaemons.java
new file mode 100644
index 0000000..499195f
--- /dev/null
+++ b/vpn/java/com/android/server/vpn/VpnDaemons.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2009, 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.vpn;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A helper class for managing native VPN daemons.
+ */
+class VpnDaemons implements Serializable {
+ static final long serialVersionUID = 1L;
+ private final String TAG = VpnDaemons.class.getSimpleName();
+
+ private static final String MTPD = "mtpd";
+ private static final String IPSEC = "racoon";
+
+ private static final String L2TP = "l2tp";
+ private static final String L2TP_PORT = "1701";
+
+ private static final String PPTP = "pptp";
+ private static final String PPTP_PORT = "1723";
+
+ private static final String VPN_LINKNAME = "vpn";
+ private static final String PPP_ARGS_SEPARATOR = "";
+
+ private List<DaemonProxy> mDaemonList = new ArrayList<DaemonProxy>();
+
+ public DaemonProxy startL2tp(String serverIp, String secret,
+ String username, String password) throws IOException {
+ return startMtpd(L2TP, serverIp, L2TP_PORT, secret, username, password,
+ false);
+ }
+
+ public DaemonProxy startPptp(String serverIp, String username,
+ String password, boolean encryption) throws IOException {
+ return startMtpd(PPTP, serverIp, PPTP_PORT, null, username, password,
+ encryption);
+ }
+
+ public DaemonProxy startIpsecForL2tp(String serverIp, String pskKey)
+ throws IOException {
+ DaemonProxy ipsec = startDaemon(IPSEC);
+ ipsec.sendCommand(serverIp, L2TP_PORT, pskKey);
+ return ipsec;
+ }
+
+ public DaemonProxy startIpsecForL2tp(String serverIp, String userKeyKey,
+ String userCertKey, String caCertKey) throws IOException {
+ DaemonProxy ipsec = startDaemon(IPSEC);
+ ipsec.sendCommand(serverIp, L2TP_PORT, userKeyKey, userCertKey,
+ caCertKey);
+ return ipsec;
+ }
+
+ public synchronized void stopAll() {
+ new DaemonProxy(MTPD).stop();
+ new DaemonProxy(IPSEC).stop();
+ }
+
+ public synchronized void closeSockets() {
+ for (DaemonProxy s : mDaemonList) s.closeControlSocket();
+ }
+
+ public synchronized boolean anyDaemonStopped() {
+ for (DaemonProxy s : mDaemonList) {
+ if (s.isStopped()) {
+ Log.w(TAG, " VPN daemon gone: " + s.getName());
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public synchronized int getSocketError() {
+ for (DaemonProxy s : mDaemonList) {
+ int errCode = getResultFromSocket(s);
+ if (errCode != 0) return errCode;
+ }
+ return 0;
+ }
+
+ private synchronized DaemonProxy startDaemon(String daemonName)
+ throws IOException {
+ DaemonProxy daemon = new DaemonProxy(daemonName);
+ mDaemonList.add(daemon);
+ daemon.start();
+ return daemon;
+ }
+
+ private int getResultFromSocket(DaemonProxy s) {
+ try {
+ return s.getResultFromSocket();
+ } catch (IOException e) {
+ return -1;
+ }
+ }
+
+ private DaemonProxy startMtpd(String protocol,
+ String serverIp, String port, String secret, String username,
+ String password, boolean encryption) throws IOException {
+ ArrayList<String> args = new ArrayList<String>();
+ args.addAll(Arrays.asList(protocol, serverIp, port));
+ if (secret != null) args.add(secret);
+ args.add(PPP_ARGS_SEPARATOR);
+ addPppArguments(args, serverIp, username, password, encryption);
+
+ DaemonProxy mtpd = startDaemon(MTPD);
+ mtpd.sendCommand(args.toArray(new String[args.size()]));
+ return mtpd;
+ }
+
+ private static void addPppArguments(ArrayList<String> args, String serverIp,
+ String username, String password, boolean encryption)
+ throws IOException {
+ args.addAll(Arrays.asList(
+ "linkname", VPN_LINKNAME,
+ "name", username,
+ "password", password,
+ "refuse-eap", "nodefaultroute", "usepeerdns",
+ "idle", "1800",
+ "mtu", "1400",
+ "mru", "1400"));
+ if (encryption) {
+ args.add("+mppe");
+ }
+ }
+}
diff --git a/vpn/java/com/android/server/vpn/VpnService.java b/vpn/java/com/android/server/vpn/VpnService.java
new file mode 100644
index 0000000..4966c06
--- /dev/null
+++ b/vpn/java/com/android/server/vpn/VpnService.java
@@ -0,0 +1,477 @@
+/*
+ * Copyright (C) 2009, 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.vpn;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.net.vpn.VpnManager;
+import android.net.vpn.VpnProfile;
+import android.net.vpn.VpnState;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.R;
+
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.UnknownHostException;
+
+/**
+ * The service base class for managing a type of VPN connection.
+ */
+abstract class VpnService<E extends VpnProfile> {
+ private static final boolean DBG = true;
+ private static final int NOTIFICATION_ID = 1;
+
+ private static final String DNS1 = "net.dns1";
+ private static final String DNS2 = "net.dns2";
+ private static final String VPN_DNS1 = "vpn.dns1";
+ private static final String VPN_DNS2 = "vpn.dns2";
+ private static final String VPN_STATUS = "vpn.status";
+ private static final String VPN_IS_UP = "ok";
+ private static final String VPN_IS_DOWN = "down";
+
+ private static final String REMOTE_IP = "net.ipremote";
+ private static final String DNS_DOMAIN_SUFFICES = "net.dns.search";
+
+ private final String TAG = VpnService.class.getSimpleName();
+
+ E mProfile;
+ transient Context mContext;
+
+ private VpnState mState = VpnState.IDLE;
+ private Throwable mError;
+
+ // connection settings
+ private String mOriginalDns1;
+ private String mOriginalDns2;
+ private String mOriginalDomainSuffices;
+ private String mLocalIp;
+ private String mLocalIf;
+
+ private long mStartTime; // VPN connection start time
+
+ // for helping managing daemons
+ private VpnDaemons mDaemons = new VpnDaemons();
+
+ // for helping showing, updating notification
+ private transient NotificationHelper mNotification;
+
+ /**
+ * Establishes a VPN connection with the specified username and password.
+ */
+ protected abstract void connect(String serverIp, String username,
+ String password) throws IOException;
+
+ /**
+ * Returns the daemons management class for this service object.
+ */
+ protected VpnDaemons getDaemons() {
+ return mDaemons;
+ }
+
+ /**
+ * Returns the VPN profile associated with the connection.
+ */
+ protected E getProfile() {
+ return mProfile;
+ }
+
+ /**
+ * Returns the IP address of the specified host name.
+ */
+ protected String getIp(String hostName) throws IOException {
+ return InetAddress.getByName(hostName).getHostAddress();
+ }
+
+ void setContext(Context context, E profile) {
+ mProfile = profile;
+ mContext = context;
+ mNotification = new NotificationHelper();
+
+ if (VpnState.CONNECTED.equals(mState)) {
+ Log.i("VpnService", " recovered: " + mProfile.getName());
+ startConnectivityMonitor();
+ }
+ }
+
+ VpnState getState() {
+ return mState;
+ }
+
+ boolean isIdle() {
+ return (mState == VpnState.IDLE);
+ }
+
+ synchronized boolean onConnect(String username, String password) {
+ try {
+ setState(VpnState.CONNECTING);
+
+ mDaemons.stopAll();
+ String serverIp = getIp(getProfile().getServerName());
+ saveLocalIpAndInterface(serverIp);
+ onBeforeConnect();
+ connect(serverIp, username, password);
+ waitUntilConnectedOrTimedout();
+ return true;
+ } catch (Throwable e) {
+ onError(e);
+ return false;
+ }
+ }
+
+ synchronized void onDisconnect() {
+ try {
+ Log.i(TAG, "disconnecting VPN...");
+ setState(VpnState.DISCONNECTING);
+ mNotification.showDisconnect();
+
+ mDaemons.stopAll();
+ } catch (Throwable e) {
+ Log.e(TAG, "onDisconnect()", e);
+ } finally {
+ onFinalCleanUp();
+ }
+ }
+
+ private void onError(Throwable error) {
+ // error may occur during or after connection setup
+ // and it may be due to one or all services gone
+ if (mError != null) {
+ Log.w(TAG, " multiple errors occur, record the last one: "
+ + error);
+ }
+ Log.e(TAG, "onError()", error);
+ mError = error;
+ onDisconnect();
+ }
+
+ private void onError(int errorCode) {
+ onError(new VpnConnectingError(errorCode));
+ }
+
+
+ private void onBeforeConnect() throws IOException {
+ mNotification.disableNotification();
+
+ SystemProperties.set(VPN_DNS1, "");
+ SystemProperties.set(VPN_DNS2, "");
+ SystemProperties.set(VPN_STATUS, VPN_IS_DOWN);
+ if (DBG) {
+ Log.d(TAG, " VPN UP: " + SystemProperties.get(VPN_STATUS));
+ }
+ }
+
+ private void waitUntilConnectedOrTimedout() throws IOException {
+ sleep(2000); // 2 seconds
+ for (int i = 0; i < 80; i++) {
+ if (mState != VpnState.CONNECTING) {
+ break;
+ } else if (VPN_IS_UP.equals(
+ SystemProperties.get(VPN_STATUS))) {
+ onConnected();
+ return;
+ } else {
+ int err = mDaemons.getSocketError();
+ if (err != 0) {
+ onError(err);
+ return;
+ }
+ }
+ sleep(500); // 0.5 second
+ }
+
+ if (mState == VpnState.CONNECTING) {
+ onError(new IOException("Connecting timed out"));
+ }
+ }
+
+ private synchronized void onConnected() throws IOException {
+ if (DBG) Log.d(TAG, "onConnected()");
+
+ mDaemons.closeSockets();
+ saveOriginalDns();
+ saveAndSetDomainSuffices();
+
+ mStartTime = System.currentTimeMillis();
+
+ setState(VpnState.CONNECTED);
+ setVpnDns();
+
+ startConnectivityMonitor();
+ }
+
+ private synchronized void onFinalCleanUp() {
+ if (DBG) Log.d(TAG, "onFinalCleanUp()");
+
+ if (mState == VpnState.IDLE) return;
+
+ // keep the notification when error occurs
+ if (!anyError()) mNotification.disableNotification();
+
+ restoreOriginalDns();
+ restoreOriginalDomainSuffices();
+ setState(VpnState.IDLE);
+
+ SystemProperties.set(VPN_STATUS, VPN_IS_DOWN);
+ }
+
+ private boolean anyError() {
+ return (mError != null);
+ }
+
+ private void restoreOriginalDns() {
+ // restore only if they are not overridden
+ String vpnDns1 = SystemProperties.get(VPN_DNS1);
+ if (vpnDns1.equals(SystemProperties.get(DNS1))) {
+ Log.i(TAG, String.format("restore original dns prop: %s --> %s",
+ SystemProperties.get(DNS1), mOriginalDns1));
+ Log.i(TAG, String.format("restore original dns prop: %s --> %s",
+ SystemProperties.get(DNS2), mOriginalDns2));
+ SystemProperties.set(DNS1, mOriginalDns1);
+ SystemProperties.set(DNS2, mOriginalDns2);
+ }
+ }
+
+ private void saveOriginalDns() {
+ mOriginalDns1 = SystemProperties.get(DNS1);
+ mOriginalDns2 = SystemProperties.get(DNS2);
+ Log.i(TAG, String.format("save original dns prop: %s, %s",
+ mOriginalDns1, mOriginalDns2));
+ }
+
+ private void setVpnDns() {
+ String vpnDns1 = SystemProperties.get(VPN_DNS1);
+ String vpnDns2 = SystemProperties.get(VPN_DNS2);
+ SystemProperties.set(DNS1, vpnDns1);
+ SystemProperties.set(DNS2, vpnDns2);
+ Log.i(TAG, String.format("set vpn dns prop: %s, %s",
+ vpnDns1, vpnDns2));
+ }
+
+ private void saveAndSetDomainSuffices() {
+ mOriginalDomainSuffices = SystemProperties.get(DNS_DOMAIN_SUFFICES);
+ Log.i(TAG, "save original suffices: " + mOriginalDomainSuffices);
+ String list = mProfile.getDomainSuffices();
+ if (!TextUtils.isEmpty(list)) {
+ SystemProperties.set(DNS_DOMAIN_SUFFICES, list);
+ }
+ }
+
+ private void restoreOriginalDomainSuffices() {
+ Log.i(TAG, "restore original suffices --> " + mOriginalDomainSuffices);
+ SystemProperties.set(DNS_DOMAIN_SUFFICES, mOriginalDomainSuffices);
+ }
+
+ private void setState(VpnState newState) {
+ mState = newState;
+ broadcastConnectivity(newState);
+ }
+
+ private void broadcastConnectivity(VpnState s) {
+ VpnManager m = new VpnManager(mContext);
+ Throwable err = mError;
+ if ((s == VpnState.IDLE) && (err != null)) {
+ if (err instanceof UnknownHostException) {
+ m.broadcastConnectivity(mProfile.getName(), s,
+ VpnManager.VPN_ERROR_UNKNOWN_SERVER);
+ } else if (err instanceof VpnConnectingError) {
+ m.broadcastConnectivity(mProfile.getName(), s,
+ ((VpnConnectingError) err).getErrorCode());
+ } else if (VPN_IS_UP.equals(SystemProperties.get(VPN_STATUS))) {
+ m.broadcastConnectivity(mProfile.getName(), s,
+ VpnManager.VPN_ERROR_CONNECTION_LOST);
+ } else {
+ m.broadcastConnectivity(mProfile.getName(), s,
+ VpnManager.VPN_ERROR_CONNECTION_FAILED);
+ }
+ } else {
+ m.broadcastConnectivity(mProfile.getName(), s);
+ }
+ }
+
+ private void startConnectivityMonitor() {
+ new Thread(new Runnable() {
+ public void run() {
+ Log.i(TAG, "VPN connectivity monitor running");
+ try {
+ mNotification.update(mStartTime); // to pop up notification
+ for (int i = 10; ; i--) {
+ long now = System.currentTimeMillis();
+
+ boolean heavyCheck = i == 0;
+ synchronized (VpnService.this) {
+ if (mState != VpnState.CONNECTED) break;
+ mNotification.update(now);
+
+ if (heavyCheck) {
+ i = 10;
+ if (checkConnectivity()) checkDns();
+ }
+ long t = 1000L - System.currentTimeMillis() + now;
+ if (t > 100L) VpnService.this.wait(t);
+ }
+ }
+ } catch (InterruptedException e) {
+ onError(e);
+ }
+ Log.i(TAG, "VPN connectivity monitor stopped");
+ }
+ }).start();
+ }
+
+ private void saveLocalIpAndInterface(String serverIp) throws IOException {
+ DatagramSocket s = new DatagramSocket();
+ int port = 80; // arbitrary
+ s.connect(InetAddress.getByName(serverIp), port);
+ InetAddress localIp = s.getLocalAddress();
+ mLocalIp = localIp.getHostAddress();
+ NetworkInterface localIf = NetworkInterface.getByInetAddress(localIp);
+ mLocalIf = (localIf == null) ? null : localIf.getName();
+ if (TextUtils.isEmpty(mLocalIf)) {
+ throw new IOException("Local interface is empty!");
+ }
+ if (DBG) {
+ Log.d(TAG, " Local IP: " + mLocalIp + ", if: " + mLocalIf);
+ }
+ }
+
+ // returns false if vpn connectivity is broken
+ private boolean checkConnectivity() {
+ if (mDaemons.anyDaemonStopped() || isLocalIpChanged()) {
+ onError(new IOException("Connectivity lost"));
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ private void checkDns() {
+ String dns1 = SystemProperties.get(DNS1);
+ String vpnDns1 = SystemProperties.get(VPN_DNS1);
+ if (!dns1.equals(vpnDns1) && dns1.equals(mOriginalDns1)) {
+ // dhcp expires?
+ setVpnDns();
+ }
+ }
+
+ private boolean isLocalIpChanged() {
+ try {
+ InetAddress localIp = InetAddress.getByName(mLocalIp);
+ NetworkInterface localIf =
+ NetworkInterface.getByInetAddress(localIp);
+ if (localIf == null || !mLocalIf.equals(localIf.getName())) {
+ Log.w(TAG, " local If changed from " + mLocalIf
+ + " to " + localIf);
+ return true;
+ } else {
+ return false;
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "isLocalIpChanged()", e);
+ return true;
+ }
+ }
+
+ protected void sleep(int ms) {
+ try {
+ Thread.currentThread().sleep(ms);
+ } catch (InterruptedException e) {
+ }
+ }
+
+ // Helper class for showing, updating notification.
+ private class NotificationHelper {
+ private NotificationManager mNotificationManager = (NotificationManager)
+ mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ private Notification mNotification =
+ new Notification(R.drawable.vpn_connected, null, 0L);
+ private PendingIntent mPendingIntent = PendingIntent.getActivity(
+ mContext, 0,
+ new VpnManager(mContext).createSettingsActivityIntent(), 0);
+ private String mConnectedTitle;
+
+ void update(long now) {
+ Notification n = mNotification;
+ if (now == mStartTime) {
+ // to pop up the notification for the first time
+ n.when = mStartTime;
+ n.tickerText = mConnectedTitle = getNotificationTitle(true);
+ } else {
+ n.tickerText = null;
+ }
+ n.setLatestEventInfo(mContext, mConnectedTitle,
+ getConnectedNotificationMessage(now),
+ mPendingIntent);
+ n.flags |= Notification.FLAG_NO_CLEAR;
+ n.flags |= Notification.FLAG_ONGOING_EVENT;
+ enableNotification(n);
+ }
+
+ void showDisconnect() {
+ String title = getNotificationTitle(false);
+ Notification n = new Notification(R.drawable.vpn_disconnected,
+ title, System.currentTimeMillis());
+ n.setLatestEventInfo(mContext, title,
+ getDisconnectedNotificationMessage(),
+ mPendingIntent);
+ n.flags |= Notification.FLAG_AUTO_CANCEL;
+ disableNotification();
+ enableNotification(n);
+ }
+
+ void disableNotification() {
+ mNotificationManager.cancel(NOTIFICATION_ID);
+ }
+
+ private void enableNotification(Notification n) {
+ mNotificationManager.notify(NOTIFICATION_ID, n);
+ }
+
+ private String getNotificationTitle(boolean connected) {
+ String formatString = connected
+ ? mContext.getString(
+ R.string.vpn_notification_title_connected)
+ : mContext.getString(
+ R.string.vpn_notification_title_disconnected);
+ return String.format(formatString, mProfile.getName());
+ }
+
+ private String getFormattedTime(int duration) {
+ int hours = duration / 3600;
+ StringBuilder sb = new StringBuilder();
+ if (hours > 0) sb.append(hours).append(':');
+ sb.append(String.format("%02d:%02d", (duration % 3600 / 60),
+ (duration % 60)));
+ return sb.toString();
+ }
+
+ private String getConnectedNotificationMessage(long now) {
+ return getFormattedTime((int) (now - mStartTime) / 1000);
+ }
+
+ private String getDisconnectedNotificationMessage() {
+ return mContext.getString(
+ R.string.vpn_notification_hint_disconnected);
+ }
+ }
+}
diff --git a/vpn/java/com/android/server/vpn/VpnServiceBinder.java b/vpn/java/com/android/server/vpn/VpnServiceBinder.java
new file mode 100644
index 0000000..c474ff9
--- /dev/null
+++ b/vpn/java/com/android/server/vpn/VpnServiceBinder.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2009, 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.vpn;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.net.vpn.IVpnService;
+import android.net.vpn.L2tpIpsecProfile;
+import android.net.vpn.L2tpIpsecPskProfile;
+import android.net.vpn.L2tpProfile;
+import android.net.vpn.PptpProfile;
+import android.net.vpn.VpnManager;
+import android.net.vpn.VpnProfile;
+import android.net.vpn.VpnState;
+import android.util.Log;
+
+/**
+ * The service class for managing a VPN connection. It implements the
+ * {@link IVpnService} binder interface.
+ */
+public class VpnServiceBinder extends IVpnService.Stub {
+ private static final String TAG = VpnServiceBinder.class.getSimpleName();
+ private static final boolean DBG = true;
+
+ // The actual implementation is delegated to the VpnService class.
+ private VpnService<? extends VpnProfile> mService;
+
+ private Context mContext;
+
+ public VpnServiceBinder(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public synchronized boolean connect(VpnProfile p, final String username,
+ final String password) {
+ if ((mService != null) && !mService.isIdle()) return false;
+ final VpnService s = mService = createService(p);
+
+ new Thread(new Runnable() {
+ public void run() {
+ s.onConnect(username, password);
+ }
+ }).start();
+ return true;
+ }
+
+ @Override
+ public synchronized void disconnect() {
+ if (mService == null) return;
+ final VpnService s = mService;
+ mService = null;
+
+ new Thread(new Runnable() {
+ public void run() {
+ s.onDisconnect();
+ }
+ }).start();
+ }
+
+ @Override
+ public synchronized String getState(VpnProfile p) {
+ if ((mService == null)
+ || (!p.getName().equals(mService.mProfile.getName()))) {
+ return VpnState.IDLE.toString();
+ } else {
+ return mService.getState().toString();
+ }
+ }
+
+ @Override
+ public synchronized boolean isIdle() {
+ return (mService == null || mService.isIdle());
+ }
+
+ private VpnService<? extends VpnProfile> createService(VpnProfile p) {
+ switch (p.getType()) {
+ case L2TP:
+ L2tpService l2tp = new L2tpService();
+ l2tp.setContext(mContext, (L2tpProfile) p);
+ return l2tp;
+
+ case PPTP:
+ PptpService pptp = new PptpService();
+ pptp.setContext(mContext, (PptpProfile) p);
+ return pptp;
+
+ case L2TP_IPSEC_PSK:
+ L2tpIpsecPskService psk = new L2tpIpsecPskService();
+ psk.setContext(mContext, (L2tpIpsecPskProfile) p);
+ return psk;
+
+ case L2TP_IPSEC:
+ L2tpIpsecService l2tpIpsec = new L2tpIpsecService();
+ l2tpIpsec.setContext(mContext, (L2tpIpsecProfile) p);
+ return l2tpIpsec;
+
+ default:
+ return null;
+ }
+ }
+}