Simplify the VPN service implementation.

+ Remove NormalProcessProxy and ProcessProxy as they are not used
  anymore.
+ Rename AndroidServiceProxy to DaemonProxy and simplify its
  implementation as it does not extend to ProcessProxy anymore.
+ Execute connect() in VpnService in one thread, which simplifies socket
  and error handling.
+ Modify service subclasses accordingly.
+ Execute connect() and disconnect() in VpnServiceBinder so that the
  operations do not block the UI thread. Mark service as foreground only upon
  connecting.
This commit is contained in:
Hung-ying Tyan
2009-07-23 07:37:27 +08:00
parent 11b6a29dfe
commit 21bd4af88a
9 changed files with 371 additions and 741 deletions

View File

@@ -1,261 +0,0 @@
/*
* 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;
/**
* Proxy to start, stop and interact with an Android service defined in init.rc.
* The android service is expected to accept connection through Unix domain
* socket. When the proxy successfully starts the service, it will establish a
* socket connection with the service. The socket serves two purposes: (1) send
* commands to the service; (2) for the proxy to know whether the service is
* alive.
*
* After the service receives commands from the proxy, it should return either
* 0 if the service will close the socket (and the proxy will re-establish
* another connection immediately after), or 1 if the socket is remained alive.
*/
public class AndroidServiceProxy extends ProcessProxy {
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 static final int STOP_SERVICE = -1;
private static final int AUTH_ERROR_CODE = 51;
private String mServiceName;
private String mSocketName;
private LocalSocket mKeepaliveSocket;
private boolean mControlSocketInUse;
private Integer mSocketResult = null;
private String mTag;
/**
* Creates a proxy with the service name.
* @param serviceName the service name
*/
public AndroidServiceProxy(String serviceName) {
mServiceName = serviceName;
mSocketName = serviceName;
mTag = "SProxy_" + serviceName;
}
@Override
public String getName() {
return "Service " + mServiceName;
}
@Override
public synchronized void stop() {
if (isRunning()) {
try {
setResultAndCloseControlSocket(STOP_SERVICE);
} catch (IOException e) {
// should not occur
throw new RuntimeException(e);
}
}
Log.d(mTag, "----- Stop: " + mServiceName);
SystemProperties.set(SVC_STOP_CMD, mServiceName);
}
/**
* Sends a command with arguments to the service through the control socket.
*/
public synchronized void sendCommand(String ...args) throws IOException {
OutputStream out = getControlSocketOutput();
for (String arg : args) outputString(out, arg);
out.write(END_OF_ARGUMENTS);
out.flush();
checkSocketResult();
}
/**
* {@inheritDoc}
* The method returns when the service exits.
*/
@Override
protected void performTask() throws IOException {
String svc = mServiceName;
Log.d(mTag, "----- Stop the daemon just in case: " + mServiceName);
SystemProperties.set(SVC_STOP_CMD, mServiceName);
if (!blockUntil(SVC_STATE_STOPPED, 5)) {
throw new IOException("cannot start service anew: " + svc
+ ", it is still running");
}
Log.d(mTag, "+++++ Start: " + svc);
SystemProperties.set(SVC_START_CMD, svc);
boolean success = blockUntil(SVC_STATE_RUNNING, WAITING_TIME);
if (success) {
Log.d(mTag, "----- Running: " + svc + ", create keepalive socket");
LocalSocket s = mKeepaliveSocket = createServiceSocket();
setState(ProcessState.RUNNING);
if (s == null) {
// no socket connection, stop hosting the service
stop();
return;
}
try {
for (;;) {
InputStream in = s.getInputStream();
int data = in.read();
if (data >= 0) {
Log.d(mTag, "got data from control socket: " + data);
setSocketResult(data);
} else {
// service is gone
if (mControlSocketInUse) setSocketResult(-1);
break;
}
}
Log.d(mTag, "control connection closed");
} catch (IOException e) {
if (e instanceof VpnConnectingError) {
throw e;
} else {
Log.d(mTag, "control socket broken: " + e.getMessage());
}
}
// Wait 5 seconds for the service to exit
success = blockUntil(SVC_STATE_STOPPED, 5);
Log.d(mTag, "stopping " + svc + ", success? " + success);
} else {
setState(ProcessState.STOPPED);
throw new IOException("cannot start service: " + svc);
}
}
private LocalSocket createServiceSocket() throws IOException {
LocalSocket s = new LocalSocket();
LocalSocketAddress a = new LocalSocketAddress(mSocketName,
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) {
Log.d(mTag, "service not yet listen()ing; try again");
excp = e;
sleep(500);
}
}
throw excp;
}
private OutputStream getControlSocketOutput() throws IOException {
if (mKeepaliveSocket != null) {
mControlSocketInUse = true;
mSocketResult = null;
return mKeepaliveSocket.getOutputStream();
} else {
throw new IOException("no control socket available");
}
}
private void checkSocketResult() throws IOException {
try {
// will be notified when the result comes back from service
if (mSocketResult == null) wait();
} catch (InterruptedException e) {
Log.d(mTag, "checkSocketResult(): " + e);
} finally {
mControlSocketInUse = false;
if ((mSocketResult == null) || (mSocketResult < 0)) {
throw new IOException("socket error, result from service: "
+ mSocketResult);
}
}
}
private synchronized void setSocketResult(int result)
throws VpnConnectingError {
if (mControlSocketInUse) {
mSocketResult = result;
notifyAll();
} else if (result > 0) {
// error from daemon
throw new VpnConnectingError((result == AUTH_ERROR_CODE)
? VpnManager.VPN_ERROR_AUTH
: VpnManager.VPN_ERROR_CONNECTION_FAILED);
}
}
private void setResultAndCloseControlSocket(int result)
throws VpnConnectingError {
setSocketResult(result);
try {
mKeepaliveSocket.shutdownInput();
mKeepaliveSocket.shutdownOutput();
mKeepaliveSocket.close();
} catch (IOException e) {
Log.e(mTag, "close keepalive socket", e);
} finally {
mKeepaliveSocket = null;
}
}
/**
* 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 + mServiceName;
int sleepTime = 200; // ms
int n = waitTime * 1000 / sleepTime;
for (int i = 0; i < n; i++) {
if (expectedState.equals(SystemProperties.get(cmd))) {
Log.d(mTag, mServiceName + " 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();
}
}

View File

@@ -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;
/**
* 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 {
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 LocalSocket mControlSocket;
private String mTag;
/**
* 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.d(mTag, "----- Stop the daemon just in case: " + mName);
SystemProperties.set(SVC_STOP_CMD, mName);
if (!blockUntil(SVC_STATE_STOPPED, 5)) {
throw new IOException("cannot start service anew: " + svc
+ ", it is still running");
}
Log.d(mTag, "+++++ Start: " + 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.e(mTag, "close control socket", e);
} finally {
mControlSocket = null;
}
}
void stop() {
String svc = mName;
Log.d(mTag, "----- Stop: " + svc);
SystemProperties.set(SVC_STOP_CMD, svc);
boolean success = blockUntil(SVC_STATE_STOPPED, 5);
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.d(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) {
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))) {
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);
}
}
}

View File

@@ -25,7 +25,7 @@ import java.io.IOException;
* connection. * connection.
*/ */
class L2tpIpsecPskService extends VpnService<L2tpIpsecPskProfile> { class L2tpIpsecPskService extends VpnService<L2tpIpsecPskProfile> {
private static final String IPSEC_DAEMON = "racoon"; private static final String IPSEC = "racoon";
@Override @Override
protected void connect(String serverIp, String username, String password) protected void connect(String serverIp, String username, String password)
@@ -33,9 +33,9 @@ class L2tpIpsecPskService extends VpnService<L2tpIpsecPskProfile> {
L2tpIpsecPskProfile p = getProfile(); L2tpIpsecPskProfile p = getProfile();
// IPSEC // IPSEC
AndroidServiceProxy ipsecService = startService(IPSEC_DAEMON); DaemonProxy ipsec = startDaemon(IPSEC);
ipsecService.sendCommand(serverIp, L2tpService.L2TP_PORT, ipsec.sendCommand(serverIp, L2tpService.L2TP_PORT, p.getPresharedKey());
p.getPresharedKey()); ipsec.closeControlSocket();
sleep(2000); // 2 seconds sleep(2000); // 2 seconds

View File

@@ -25,15 +25,16 @@ import java.io.IOException;
* The service that manages the certificate based L2TP-over-IPSec VPN connection. * The service that manages the certificate based L2TP-over-IPSec VPN connection.
*/ */
class L2tpIpsecService extends VpnService<L2tpIpsecProfile> { class L2tpIpsecService extends VpnService<L2tpIpsecProfile> {
private static final String IPSEC_DAEMON = "racoon"; private static final String IPSEC = "racoon";
@Override @Override
protected void connect(String serverIp, String username, String password) protected void connect(String serverIp, String username, String password)
throws IOException { throws IOException {
// IPSEC // IPSEC
AndroidServiceProxy ipsecService = startService(IPSEC_DAEMON); DaemonProxy ipsec = startDaemon(IPSEC);
ipsecService.sendCommand(serverIp, L2tpService.L2TP_PORT, ipsec.sendCommand(serverIp, L2tpService.L2TP_PORT,
getUserkeyPath(), getUserCertPath(), getCaCertPath()); getUserkeyPath(), getUserCertPath(), getCaCertPath());
ipsec.closeControlSocket();
sleep(2000); // 2 seconds sleep(2000); // 2 seconds

View File

@@ -24,7 +24,7 @@ import java.util.Arrays;
* A helper class for sending commands to the MTP daemon (mtpd). * A helper class for sending commands to the MTP daemon (mtpd).
*/ */
class MtpdHelper { class MtpdHelper {
private static final String MTPD_SERVICE = "mtpd"; private static final String MTPD = "mtpd";
private static final String VPN_LINKNAME = "vpn"; private static final String VPN_LINKNAME = "vpn";
private static final String PPP_ARGS_SEPARATOR = ""; private static final String PPP_ARGS_SEPARATOR = "";
@@ -37,7 +37,7 @@ class MtpdHelper {
args.add(PPP_ARGS_SEPARATOR); args.add(PPP_ARGS_SEPARATOR);
addPppArguments(vpnService, args, serverIp, username, password); addPppArguments(vpnService, args, serverIp, username, password);
AndroidServiceProxy mtpd = vpnService.startService(MTPD_SERVICE); DaemonProxy mtpd = vpnService.startDaemon(MTPD);
mtpd.sendCommand(args.toArray(new String[args.size()])); mtpd.sendCommand(args.toArray(new String[args.size()]));
} }

View File

@@ -1,85 +0,0 @@
/*
* 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.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
/**
* Proxy to perform a command with arguments.
*/
public class NormalProcessProxy extends ProcessProxy {
private Process mProcess;
private String[] mArgs;
private String mTag;
/**
* Creates a proxy with the arguments.
* @param args the argument list with the first one being the command
*/
public NormalProcessProxy(String ...args) {
if ((args == null) || (args.length == 0)) {
throw new IllegalArgumentException();
}
mArgs = args;
mTag = "PProxy_" + getName();
}
@Override
public String getName() {
return mArgs[0];
}
@Override
public synchronized void stop() {
if (isStopped()) return;
getHostThread().interrupt();
// TODO: not sure how to reliably kill a process
mProcess.destroy();
setState(ProcessState.STOPPING);
}
@Override
protected void performTask() throws IOException, InterruptedException {
String[] args = mArgs;
Log.d(mTag, "+++++ Execute: " + getEntireCommand());
ProcessBuilder pb = new ProcessBuilder(args);
setState(ProcessState.RUNNING);
Process p = mProcess = pb.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(
p.getInputStream()));
while (true) {
String line = reader.readLine();
if ((line == null) || isStopping()) break;
Log.d(mTag, line);
}
Log.d(mTag, "----- p.waitFor(): " + getName());
p.waitFor();
Log.d(mTag, "----- Done: " + getName());
}
private CharSequence getEntireCommand() {
String[] args = mArgs;
StringBuilder sb = new StringBuilder(args[0]);
for (int i = 1; i < args.length; i++) sb.append(' ').append(args[i]);
return sb;
}
}

View File

@@ -1,210 +0,0 @@
/*
* 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.os.ConditionVariable;
import java.io.IOException;
/**
* A proxy class that spawns a process to accomplish a certain task.
*/
public abstract class ProcessProxy {
/**
* Defines the interface to call back when the process is finished or an
* error occurs during execution.
*/
public static interface Callback {
/**
* Called when the process is finished.
* @param proxy the proxy that hosts the process
*/
void done(ProcessProxy proxy);
/**
* Called when some error occurs.
* @param proxy the proxy that hosts the process
*/
void error(ProcessProxy proxy, Throwable error);
}
protected enum ProcessState {
STOPPED, STARTING, RUNNING, STOPPING, ERROR
}
private ProcessState mState = ProcessState.STOPPED;
private ConditionVariable mLock = new ConditionVariable();
private Thread mThread;
/**
* Returns the name of the process.
*/
public abstract String getName();
/**
* Starts the process with a callback.
* @param callback the callback to get notified when the process is finished
* or an error occurs during execution
* @throws IOException when the process is already running or failed to
* start
*/
public synchronized void start(final Callback callback) throws IOException {
if (!isStopped()) {
throw new IOException("Process is already running: " + this);
}
mLock.close();
setState(ProcessState.STARTING);
Thread thread = new Thread(new Runnable() {
public void run() {
try {
performTask();
setState(ProcessState.STOPPED);
mLock.open();
if (callback != null) callback.done(ProcessProxy.this);
} catch (Throwable e) {
setState(ProcessState.ERROR);
if (callback != null) callback.error(ProcessProxy.this, e);
} finally {
mThread = null;
}
}
});
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
mThread = thread;
if (!waitUntilRunning()) {
throw new IOException("Failed to start the process: " + this);
}
}
/**
* Starts the process.
* @throws IOException when the process is already running or failed to
* start
*/
public synchronized void start() throws IOException {
start(null);
if (!waitUntilDone()) {
throw new IOException("Failed to complete the process: " + this);
}
}
/**
* Returns the thread that hosts the process.
*/
public Thread getHostThread() {
return mThread;
}
/**
* Blocks the current thread until the hosted process is finished.
*
* @return true if the process is finished normally; false if an error
* occurs
*/
public boolean waitUntilDone() {
while (!mLock.block(1000)) {
if (isStopped() || isInError()) break;
}
return isStopped();
}
/**
* Blocks the current thread until the hosted process is running.
*
* @return true if the process is running normally; false if the process
* is in another state
*/
private boolean waitUntilRunning() {
for (;;) {
if (!isStarting()) break;
}
return isRunning();
}
/**
* Stops and destroys the process.
*/
public abstract void stop();
/**
* Checks whether the process is finished.
* @return true if the process is stopped
*/
public boolean isStopped() {
return (mState == ProcessState.STOPPED);
}
/**
* Checks whether the process is being stopped.
* @return true if the process is being stopped
*/
public boolean isStopping() {
return (mState == ProcessState.STOPPING);
}
/**
* Checks whether the process is being started.
* @return true if the process is being started
*/
public boolean isStarting() {
return (mState == ProcessState.STARTING);
}
/**
* Checks whether the process is running.
* @return true if the process is running
*/
public boolean isRunning() {
return (mState == ProcessState.RUNNING);
}
/**
* Checks whether some error has occurred and the process is stopped.
* @return true if some error has occurred and the process is stopped
*/
public boolean isInError() {
return (mState == ProcessState.ERROR);
}
/**
* Performs the actual task. Subclasses must make sure that the method
* is blocked until the task is done or an error occurs.
*/
protected abstract void performTask()
throws IOException, InterruptedException;
/**
* Sets the process state.
* @param state the new state to be in
*/
protected void setState(ProcessState state) {
mState = state;
}
/**
* Makes the current thread sleep for the specified time.
* @param msec time to sleep in miliseconds
*/
protected void sleep(int msec) {
try {
Thread.currentThread().sleep(msec);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -50,14 +50,15 @@ abstract class VpnService<E extends VpnProfile> {
private static final String REMOTE_IP = "net.ipremote"; private static final String REMOTE_IP = "net.ipremote";
private static final String DNS_DOMAIN_SUFFICES = "net.dns.search"; private static final String DNS_DOMAIN_SUFFICES = "net.dns.search";
private static final int AUTH_ERROR_CODE = 51;
private final String TAG = VpnService.class.getSimpleName(); private final String TAG = VpnService.class.getSimpleName();
E mProfile; E mProfile;
VpnServiceBinder mContext; VpnServiceBinder mContext;
private VpnState mState = VpnState.IDLE; private VpnState mState = VpnState.IDLE;
private boolean mInError; private Throwable mError;
private VpnConnectingError mError;
// connection settings // connection settings
private String mOriginalDns1; private String mOriginalDns1;
@@ -68,8 +69,8 @@ abstract class VpnService<E extends VpnProfile> {
private long mStartTime; // VPN connection start time private long mStartTime; // VPN connection start time
// for helping managing multiple Android services // for helping managing multiple daemons
private ServiceHelper mServiceHelper = new ServiceHelper(); private DaemonHelper mDaemonHelper = new DaemonHelper();
// for helping showing, updating notification // for helping showing, updating notification
private NotificationHelper mNotification = new NotificationHelper(); private NotificationHelper mNotification = new NotificationHelper();
@@ -81,18 +82,11 @@ abstract class VpnService<E extends VpnProfile> {
String password) throws IOException; String password) throws IOException;
/** /**
* Tears down the VPN connection. The base class simply terminates all the * Starts a VPN daemon.
* Android services. A subclass may need to do some clean-up before that.
*/ */
protected void disconnect() { protected DaemonProxy startDaemon(String daemonName)
}
/**
* Starts an Android service defined in init.rc.
*/
protected AndroidServiceProxy startService(String serviceName)
throws IOException { throws IOException {
return mServiceHelper.startService(serviceName); return mDaemonHelper.startDaemon(daemonName);
} }
/** /**
@@ -109,28 +103,6 @@ abstract class VpnService<E extends VpnProfile> {
return InetAddress.getByName(hostName).getHostAddress(); return InetAddress.getByName(hostName).getHostAddress();
} }
/**
* Sets the system property. The method is blocked until the value is
* settled in.
* @param name the name of the property
* @param value the value of the property
* @throws IOException if it fails to set the property within 2 seconds
*/
protected void setSystemProperty(String name, String value)
throws IOException {
SystemProperties.set(name, value);
for (int i = 0; i < 5; i++) {
String v = SystemProperties.get(name);
if (v.equals(value)) {
return;
} else {
Log.d(TAG, "sys_prop: wait for " + name + " to settle in");
sleep(400);
}
}
throw new IOException("Failed to set system property: " + name);
}
void setContext(VpnServiceBinder context, E profile) { void setContext(VpnServiceBinder context, E profile) {
mContext = context; mContext = context;
mProfile = profile; mProfile = profile;
@@ -153,44 +125,42 @@ abstract class VpnService<E extends VpnProfile> {
return true; return true;
} catch (Throwable e) { } catch (Throwable e) {
Log.e(TAG, "onConnect()", e); Log.e(TAG, "onConnect()", e);
mError = newConnectingError(e); onError(e);
onError();
return false; return false;
} }
} }
synchronized void onDisconnect(boolean cleanUpServices) { synchronized void onDisconnect() {
try { try {
Log.d(TAG, " disconnecting VPN..."); Log.d(TAG, " disconnecting VPN...");
mState = VpnState.DISCONNECTING; mState = VpnState.DISCONNECTING;
broadcastConnectivity(VpnState.DISCONNECTING); broadcastConnectivity(VpnState.DISCONNECTING);
mNotification.showDisconnect(); mNotification.showDisconnect();
// subclass implementation mDaemonHelper.stopAll();
if (cleanUpServices) disconnect();
mServiceHelper.stop();
} catch (Throwable e) { } catch (Throwable e) {
Log.e(TAG, "onDisconnect()", e); Log.e(TAG, "onDisconnect()", e);
} finally {
onFinalCleanUp(); onFinalCleanUp();
} }
} }
synchronized void onError() { private void onError(Throwable error) {
// error may occur during or after connection setup // error may occur during or after connection setup
// and it may be due to one or all services gone // and it may be due to one or all services gone
mInError = true; if (mError != null) {
switch (mState) { Log.w(TAG, " multiple errors occur, record the last one: "
case CONNECTED: + error);
onDisconnect(true);
break;
case CONNECTING:
onDisconnect(false);
break;
} }
mError = error;
onDisconnect();
} }
private void onError(int errorCode) {
onError(new VpnConnectingError(errorCode));
}
private void onBeforeConnect() { private void onBeforeConnect() {
mNotification.disableNotification(); mNotification.disableNotification();
@@ -201,41 +171,39 @@ abstract class VpnService<E extends VpnProfile> {
} }
private void waitUntilConnectedOrTimedout() { private void waitUntilConnectedOrTimedout() {
// Run this in the background thread to not block UI sleep(2000); // 2 seconds
new Thread(new Runnable() { for (int i = 0; i < 60; i++) {
public void run() { if (mState != VpnState.CONNECTING) {
sleep(2000); // 2 seconds break;
for (int i = 0; i < 60; i++) { } else if (VPN_IS_UP.equals(
if (VPN_IS_UP.equals(SystemProperties.get(VPN_STATUS))) { SystemProperties.get(VPN_STATUS))) {
onConnected(); onConnected();
return; return;
} else if (mState != VpnState.CONNECTING) { } else if (mDaemonHelper.anySocketError()) {
break; return;
}
sleep(500); // 0.5 second
}
synchronized (VpnService.this) {
if (mState == VpnState.CONNECTING) {
Log.d(TAG, " connecting timed out !!");
mError = newConnectingError(
new IOException("Connecting timed out"));
onError();
}
}
} }
}).start(); sleep(500); // 0.5 second
}
synchronized (VpnService.this) {
if (mState == VpnState.CONNECTING) {
Log.d(TAG, " connecting timed out !!");
onError(new IOException("Connecting timed out"));
}
}
} }
private synchronized void onConnected() { private synchronized void onConnected() {
Log.d(TAG, "onConnected()"); Log.d(TAG, "onConnected()");
mDaemonHelper.closeSockets();
saveVpnDnsProperties(); saveVpnDnsProperties();
saveAndSetDomainSuffices(); saveAndSetDomainSuffices();
startConnectivityMonitor();
mState = VpnState.CONNECTED; mState = VpnState.CONNECTED;
broadcastConnectivity(VpnState.CONNECTED); broadcastConnectivity(VpnState.CONNECTED);
enterConnectivityLoop();
} }
private synchronized void onFinalCleanUp() { private synchronized void onFinalCleanUp() {
@@ -244,7 +212,7 @@ abstract class VpnService<E extends VpnProfile> {
if (mState == VpnState.IDLE) return; if (mState == VpnState.IDLE) return;
// keep the notification when error occurs // keep the notification when error occurs
if (!mInError) mNotification.disableNotification(); if (!anyError()) mNotification.disableNotification();
restoreOriginalDnsProperties(); restoreOriginalDnsProperties();
restoreOriginalDomainSuffices(); restoreOriginalDomainSuffices();
@@ -255,37 +223,8 @@ abstract class VpnService<E extends VpnProfile> {
mContext.stopSelf(); mContext.stopSelf();
} }
private VpnConnectingError newConnectingError(Throwable e) { private boolean anyError() {
return new VpnConnectingError( return (mError != null);
(e instanceof UnknownHostException)
? VpnManager.VPN_ERROR_UNKNOWN_SERVER
: VpnManager.VPN_ERROR_CONNECTION_FAILED);
}
private synchronized void onOneServiceGone() {
switch (mState) {
case IDLE:
case DISCONNECTING:
break;
default:
onError();
}
}
private synchronized void onAllServicesGone() {
switch (mState) {
case IDLE:
break;
case DISCONNECTING:
// daemons are gone; now clean up everything
onFinalCleanUp();
break;
default:
onError();
}
} }
private void restoreOriginalDnsProperties() { private void restoreOriginalDnsProperties() {
@@ -341,46 +280,65 @@ abstract class VpnService<E extends VpnProfile> {
private void broadcastConnectivity(VpnState s) { private void broadcastConnectivity(VpnState s) {
VpnManager m = new VpnManager(mContext); VpnManager m = new VpnManager(mContext);
if ((s == VpnState.IDLE) && (mError != null)) { Throwable err = mError;
m.broadcastConnectivity(mProfile.getName(), s, if ((s == VpnState.IDLE) && (err != null)) {
mError.getErrorCode()); 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 {
m.broadcastConnectivity(mProfile.getName(), s,
VpnManager.VPN_ERROR_CONNECTION_FAILED);
}
} else { } else {
m.broadcastConnectivity(mProfile.getName(), s); m.broadcastConnectivity(mProfile.getName(), s);
} }
} }
private void startConnectivityMonitor() { private void enterConnectivityLoop() {
mStartTime = System.currentTimeMillis(); mStartTime = System.currentTimeMillis();
new Thread(new Runnable() { Log.d(TAG, " +++++ connectivity monitor running");
public void run() { try {
Log.d(TAG, " +++++ connectivity monitor running"); for (;;) {
try { synchronized (VpnService.this) {
for (;;) { if (mState != VpnState.CONNECTED) break;
synchronized (VpnService.this) { mNotification.update();
if (mState != VpnState.CONNECTED) break; checkConnectivity();
mNotification.update(); VpnService.this.wait(1000); // 1 second
checkConnectivity();
VpnService.this.wait(1000); // 1 second
}
}
} catch (InterruptedException e) {
Log.e(TAG, "connectivity monitor", e);
} }
Log.d(TAG, " ----- connectivity monitor stopped");
} }
}).start(); } catch (InterruptedException e) {
Log.e(TAG, "connectivity monitor", e);
}
Log.d(TAG, " ----- connectivity monitor stopped");
} }
private void checkConnectivity() { private void checkConnectivity() {
checkDnsProperties(); if (mDaemonHelper.anyDaemonStopped() || isLocalIpChanged()) {
onDisconnect();
}
} }
private void checkDnsProperties() { private boolean isLocalIpChanged() {
// TODO
if (!isDnsIntact()) {
Log.w(TAG, " local IP changed");
return true;
} else {
return false;
}
}
private boolean isDnsIntact() {
String dns1 = SystemProperties.get(DNS1); String dns1 = SystemProperties.get(DNS1);
if (!mVpnDns1.equals(dns1)) { if (!mVpnDns1.equals(dns1)) {
Log.w(TAG, " dns being overridden by: " + dns1); Log.w(TAG, " dns being overridden by: " + dns1);
onError(); return false;
} else {
return true;
} }
} }
@@ -391,56 +349,64 @@ abstract class VpnService<E extends VpnProfile> {
} }
} }
private InetAddress toInetAddress(int addr) throws IOException { private class DaemonHelper {
byte[] aa = new byte[4]; private List<DaemonProxy> mDaemonList =
for (int i= 0; i < aa.length; i++) { new ArrayList<DaemonProxy>();
aa[i] = (byte) (addr & 0x0FF);
addr >>= 8;
}
return InetAddress.getByAddress(aa);
}
private class ServiceHelper implements ProcessProxy.Callback { synchronized DaemonProxy startDaemon(String daemonName)
private List<AndroidServiceProxy> mServiceList =
new ArrayList<AndroidServiceProxy>();
// starts an Android service
AndroidServiceProxy startService(String serviceName)
throws IOException { throws IOException {
AndroidServiceProxy service = new AndroidServiceProxy(serviceName); DaemonProxy daemon = new DaemonProxy(daemonName);
mServiceList.add(service); mDaemonList.add(daemon);
service.start(this); daemon.start();
return service; return daemon;
} }
// stops all the Android services synchronized void stopAll() {
void stop() { if (mDaemonList.isEmpty()) {
if (mServiceList.isEmpty()) {
onFinalCleanUp(); onFinalCleanUp();
} else { } else {
for (AndroidServiceProxy s : mServiceList) s.stop(); for (DaemonProxy s : mDaemonList) s.stop();
} }
} }
//@Override synchronized void closeSockets() {
public void done(ProcessProxy p) { for (DaemonProxy s : mDaemonList) s.closeControlSocket();
Log.d(TAG, "service done: " + p.getName());
commonCallback((AndroidServiceProxy) p);
} }
//@Override synchronized boolean anyDaemonStopped() {
public void error(ProcessProxy p, Throwable e) { for (DaemonProxy s : mDaemonList) {
Log.e(TAG, "service error: " + p.getName(), e); if (s.isStopped()) {
if (e instanceof VpnConnectingError) { Log.w(TAG, " daemon gone: " + s.getName());
mError = (VpnConnectingError) e; return true;
}
} }
commonCallback((AndroidServiceProxy) p); return false;
} }
private void commonCallback(AndroidServiceProxy service) { private int getResultFromSocket(DaemonProxy s) {
mServiceList.remove(service); try {
onOneServiceGone(); return s.getResultFromSocket();
if (mServiceList.isEmpty()) onAllServicesGone(); } catch (IOException e) {
return -1;
}
}
synchronized boolean anySocketError() {
for (DaemonProxy s : mDaemonList) {
switch (getResultFromSocket(s)) {
case 0:
continue;
case AUTH_ERROR_CODE:
onError(VpnManager.VPN_ERROR_AUTH);
return true;
default:
onError(VpnManager.VPN_ERROR_CONNECTION_FAILED);
return true;
}
}
return false;
} }
} }

View File

@@ -42,11 +42,13 @@ public class VpnServiceBinder extends Service {
private final IBinder mBinder = new IVpnService.Stub() { private final IBinder mBinder = new IVpnService.Stub() {
public boolean connect(VpnProfile p, String username, String password) { public boolean connect(VpnProfile p, String username, String password) {
android.util.Log.d("VpnServiceBinder", "becoming foreground");
setForeground(true);
return VpnServiceBinder.this.connect(p, username, password); return VpnServiceBinder.this.connect(p, username, password);
} }
public void disconnect() { public void disconnect() {
if (mService != null) mService.onDisconnect(true); VpnServiceBinder.this.disconnect();
} }
public void checkStatus(VpnProfile p) { public void checkStatus(VpnProfile p) {
@@ -54,21 +56,39 @@ public class VpnServiceBinder extends Service {
} }
}; };
public void onStart (Intent intent, int startId) { @Override
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId); super.onStart(intent, startId);
setForeground(true);
android.util.Log.d("VpnServiceBinder", "becomes a foreground service");
} }
@Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return mBinder; return mBinder;
} }
private synchronized boolean connect( private synchronized boolean connect(final VpnProfile p,
VpnProfile p, String username, String password) { final String username, final String password) {
if (mService != null) return false; if (mService != null) return false;
mService = createService(p);
return mService.onConnect(username, password); new Thread(new Runnable() {
public void run() {
mService = createService(p);
mService.onConnect(username, password);
}
}).start();
return true;
}
private synchronized void disconnect() {
if (mService == null) return;
new Thread(new Runnable() {
public void run() {
mService.onDisconnect();
android.util.Log.d("VpnServiceBinder", "becoming background");
setForeground(false);
}
}).start();
} }
private synchronized void checkStatus(VpnProfile p) { private synchronized void checkStatus(VpnProfile p) {