diff options
author | Hung-ying Tyan <tyanh@google.com> | 2011-01-19 16:48:38 +0800 |
---|---|---|
committer | Hung-ying Tyan <tyanh@google.com> | 2011-01-20 12:51:43 +0800 |
commit | 52261a55efd313240b6fcae21b242385cf99f8b0 (patch) | |
tree | 2ee9d0d2df8a2985ac4d45b3df47990a6afdf4f4 | |
parent | c14915a2cebeab5998be2c65da69af2faa0a6c86 (diff) | |
download | frameworks_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.aidl | 13 | ||||
-rw-r--r-- | vpn/java/android/net/vpn/VpnManager.java | 106 | ||||
-rw-r--r-- | vpn/java/com/android/server/vpn/DaemonProxy.java | 199 | ||||
-rw-r--r-- | vpn/java/com/android/server/vpn/L2tpIpsecPskService.java | 47 | ||||
-rw-r--r-- | vpn/java/com/android/server/vpn/L2tpIpsecService.java | 50 | ||||
-rw-r--r-- | vpn/java/com/android/server/vpn/L2tpService.java | 35 | ||||
-rw-r--r-- | vpn/java/com/android/server/vpn/PptpService.java | 34 | ||||
-rw-r--r-- | vpn/java/com/android/server/vpn/VpnConnectingError.java | 35 | ||||
-rw-r--r-- | vpn/java/com/android/server/vpn/VpnDaemons.java | 147 | ||||
-rw-r--r-- | vpn/java/com/android/server/vpn/VpnService.java | 477 | ||||
-rw-r--r-- | vpn/java/com/android/server/vpn/VpnServiceBinder.java | 117 |
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; + } + } +} |