/*
* Copyright (C) 2012-2013 Tobias Brunner
* Copyright (C) 2012 Giuliano Grassi
* Copyright (C) 2012 Ralf Sager
* Hochschule fuer Technik Rapperswil
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
package org.strongswan.android.logic;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.security.PrivateKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import com.swisscom.safeconnect.BuildConfig;
import com.swisscom.safeconnect.activity.DashboardActivity;
import com.swisscom.safeconnect.model.PlumberAuthResponse;
import com.swisscom.safeconnect.model.PlumberStatus;
import com.swisscom.safeconnect.backend.BackendConnector;
import com.swisscom.safeconnect.utils.Config;
import com.swisscom.safeconnect.utils.VpnConfigurator;
import org.strongswan.android.data.VpnProfile;
import org.strongswan.android.data.VpnProfileDataSource;
import org.strongswan.android.data.VpnType.VpnTypeFeature;
import org.strongswan.android.logic.VpnStateService.ErrorState;
import org.strongswan.android.logic.VpnStateService.State;
import org.strongswan.android.logic.imc.ImcState;
import org.strongswan.android.logic.imc.RemediationInstruction;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.VpnService;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.security.KeyChain;
import android.security.KeyChainException;
import android.util.Log;
public class CharonVpnService extends VpnService implements Runnable
{
private static final String TAG = CharonVpnService.class.getSimpleName();
public static final String LOG_FILE = "charon.log";
private String mLogFile;
private VpnProfileDataSource mDataSource;
private Thread mConnectionHandler;
private VpnProfile mCurrentProfile;
private volatile String mCurrentCertificateAlias;
private volatile String mCurrentUserCertificateAlias;
private VpnProfile mNextProfile;
private volatile boolean mProfileUpdated;
private volatile boolean mTerminate;
private volatile boolean mIsDisconnecting;
private VpnStateService mService;
private final Object mServiceLock = new Object();
//CMA
private BackendConnector backendConnector = new BackendConnector(this);
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name)
{ /* since the service is local this is theoretically only called when the process is terminated */
synchronized (mServiceLock)
{
mService = null;
}
}
@Override
public void onServiceConnected(ComponentName name, IBinder service)
{
synchronized (mServiceLock)
{
mService = ((VpnStateService.LocalBinder)service).getService();
}
/* we are now ready to start the handler thread */
mConnectionHandler.start();
}
};
/**
* as defined in charonservice.h
*/
static final int STATE_CHILD_SA_UP = 1;
static final int STATE_CHILD_SA_DOWN = 2;
static final int STATE_AUTH_ERROR = 3;
static final int STATE_PEER_AUTH_ERROR = 4;
static final int STATE_LOOKUP_ERROR = 5;
static final int STATE_UNREACHABLE_ERROR = 6;
static final int STATE_GENERIC_ERROR = 7;
//CMA
/*
* Listen to charon state, to get notification when its disconnected
*/
private PlumberReauthenticater mPlumber;
private List<CharonListener> mCharonListener = new ArrayList<CharonListener>();
public interface CharonListener
{
public void charonStopped();
}
public void registerCharonListener(CharonListener listener)
{
mCharonListener.add(listener);
}
public void unregisterCharonListener(CharonListener listener)
{
mCharonListener.remove(listener);
}
private void notifyCharonListeners() {
for (CharonListener listener : mCharonListener) {
listener.charonStopped();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
if (intent != null)
{
Bundle bundle = intent.getExtras();
VpnProfile profile = null;
if (bundle != null)
{
//CMA
profile = (VpnProfile) bundle.getSerializable(Config.VPN_PROFILE_BUNDLE_KEY);
}
setNextProfile(profile);
}
return START_NOT_STICKY;
}
@Override
public void onCreate()
{
mLogFile = getFilesDir().getAbsolutePath() + File.separator + LOG_FILE;
mDataSource = new VpnProfileDataSource(this);
mDataSource.open();
/* use a separate thread as main thread for charon */
mConnectionHandler = new Thread(this);
/* the thread is started when the service is bound */
bindService(new Intent(this, VpnStateService.class),
mServiceConnection, Service.BIND_AUTO_CREATE);
}
@Override
public void onRevoke()
{ /* the system revoked the rights grated with the initial prepare() call.
* called when the user clicks disconnect in the system's VPN dialog */
setNextProfile(null);
}
@Override
public void onDestroy()
{
mTerminate = true;
setNextProfile(null);
try
{
mConnectionHandler.join();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
if (mService != null)
{
unbindService(mServiceConnection);
}
mDataSource.close();
}
/**
* Set the profile that is to be initiated next. Notify the handler thread.
*
* @param profile the profile to initiate
*/
private void setNextProfile(VpnProfile profile)
{
synchronized (this)
{
this.mNextProfile = profile;
mProfileUpdated = true;
notifyAll();
}
}
@Override
public void run()
{
while (true)
{
synchronized (this)
{
try
{
while (!mProfileUpdated)
{
wait();
}
mProfileUpdated = false;
stopCurrentConnection();
if (mNextProfile == null)
{
setState(State.DISABLED);
if (mTerminate)
{
break;
}
}
else
{
mCurrentProfile = mNextProfile;
mNextProfile = null;
/* store this in a separate (volatile) variable to avoid
* a possible deadlock during deinitialization */
mCurrentCertificateAlias = mCurrentProfile.getCertificateAlias();
mCurrentUserCertificateAlias = mCurrentProfile.getUserCertificateAlias();
startConnection(mCurrentProfile);
mIsDisconnecting = false;
BuilderAdapter builder = new BuilderAdapter(mCurrentProfile.getName());
if (initializeCharon(builder, mLogFile, mCurrentProfile.getVpnType().has(VpnTypeFeature.BYOD)))
{
//CMA
if (BuildConfig.DEBUG) Log.i(TAG, "charon started");
initiate(mCurrentProfile.getVpnType().getIdentifier(),
mCurrentProfile.getGateway(), mCurrentProfile.getUsername(),
//CMA
mCurrentProfile.getPassword(), mCurrentProfile.getPsk(), mCurrentProfile.getServerAuth());
}
else
{
//CMA
if (BuildConfig.DEBUG) Log.e(TAG, "failed to start charon");
setError(ErrorState.GENERIC_ERROR);
setState(State.DISABLED);
mCurrentProfile = null;
}
}
}
catch (InterruptedException ex)
{
stopCurrentConnection();
setState(State.DISABLED);
}
}
}
}
/**
* Stop any existing connection by deinitializing charon.
*/
private void stopCurrentConnection()
{
synchronized (this)
{
if (mCurrentProfile != null)
{
setState(State.DISCONNECTING);
mIsDisconnecting = true;
deinitializeCharon();
//CMA
if (BuildConfig.DEBUG) Log.i(TAG, "charon stopped");
mCurrentProfile = null;
//CMA
notifyCharonListeners();
}
}
}
/**
* Notify the state service about a new connection attempt.
* Called by the handler thread.
*
* @param profile currently active VPN profile
*/
private void startConnection(VpnProfile profile)
{
synchronized (mServiceLock)
{
if (mService != null)
{
mService.startConnection(profile);
}
}
}
/**
* Update the current VPN state on the state service. Called by the handler
* thread and any of charon's threads.
*
* @param state current state
*/
private void setState(State state)
{
synchronized (mServiceLock)
{
if (mService != null)
{
mService.setState(state);
}
}
}
/**
* Set an error on the state service. Called by the handler thread and any
* of charon's threads.
*
* @param error error state
*/
private void setError(ErrorState error)
{
synchronized (mServiceLock)
{
if (mService != null)
{
mService.setError(error);
}
}
}
/**
* Set the IMC state on the state service. Called by the handler thread and
* any of charon's threads.
*
* @param state IMC state
*/
private void setImcState(ImcState state)
{
synchronized (mServiceLock)
{
if (mService != null)
{
mService.setImcState(state);
}
}
}
/**
* Set an error on the state service. Called by the handler thread and any
* of charon's threads.
*
* @param error error state
*/
private void setErrorDisconnect(ErrorState error)
{
synchronized (mServiceLock)
{
if (mService != null)
{
if (!mIsDisconnecting)
{
mService.setError(error);
//CMA
mService.disconnect();
}
}
}
}
/**
* Updates the state of the current connection.
* Called via JNI by different threads (but not concurrently).
*
* @param status new state
*/
public void updateStatus(int status)
{
switch (status)
{
case STATE_CHILD_SA_DOWN:
if (!mIsDisconnecting)
{
setState(State.CONNECTING);
}
break;
case STATE_CHILD_SA_UP:
setState(State.CONNECTED);
break;
case STATE_AUTH_ERROR:
setErrorDisconnect(ErrorState.AUTH_FAILED);
break;
case STATE_PEER_AUTH_ERROR:
setErrorDisconnect(ErrorState.PEER_AUTH_FAILED);
break;
case STATE_LOOKUP_ERROR:
setErrorDisconnect(ErrorState.LOOKUP_FAILED);
break;
case STATE_UNREACHABLE_ERROR:
setErrorDisconnect(ErrorState.UNREACHABLE);
break;
case STATE_GENERIC_ERROR:
setErrorDisconnect(ErrorState.GENERIC_ERROR);
break;
default:
Log.e(TAG, "Unknown status code received");
break;
}
}
/**
* Updates the IMC state of the current connection.
* Called via JNI by different threads (but not concurrently).
*
* @param value new state
*/
public void updateImcState(int value)
{
ImcState state = ImcState.fromValue(value);
if (state != null)
{
setImcState(state);
}
}
/**
* Add a remediation instruction to the VPN state service.
* Called via JNI by different threads (but not concurrently).
*
* @param xml XML text
*/
public void addRemediationInstruction(String xml)
{
for (RemediationInstruction instruction : RemediationInstruction.fromXml(xml))
{
synchronized (mServiceLock)
{
if (mService != null)
{
mService.addRemediationInstruction(instruction);
}
}
}
}
/**
* Function called via JNI to generate a list of DER encoded CA certificates
* as byte array.
*
* @return a list of DER encoded CA certificates
*/
private byte[][] getTrustedCertificates()
{
ArrayList<byte[]> certs = new ArrayList<byte[]>();
TrustedCertificateManager certman = TrustedCertificateManager.getInstance();
try
{
String alias = this.mCurrentCertificateAlias;
if (alias != null)
{
X509Certificate cert = certman.getCACertificateFromAlias(alias);
if (cert == null)
{
return null;
}
certs.add(cert.getEncoded());
}
else
{
for (X509Certificate cert : certman.getAllCACertificates().values())
{
certs.add(cert.getEncoded());
}
}
}
catch (CertificateEncodingException e)
{
e.printStackTrace();
return null;
}
return certs.toArray(new byte[certs.size()][]);
}
/**
* Function called via JNI to get a list containing the DER encoded certificates
* of the user selected certificate chain (beginning with the user certificate).
*
* Since this method is called from a thread of charon's thread pool we are safe
* to call methods on KeyChain directly.
*
* @return list containing the certificates (first element is the user certificate)
* @throws InterruptedException
* @throws KeyChainException
* @throws CertificateEncodingException
*/
private byte[][] getUserCertificate() throws KeyChainException, InterruptedException, CertificateEncodingException
{
ArrayList<byte[]> encodings = new ArrayList<byte[]>();
X509Certificate[] chain = KeyChain.getCertificateChain(getApplicationContext(), mCurrentUserCertificateAlias);
if (chain == null || chain.length == 0)
{
return null;
}
for (X509Certificate cert : chain)
{
encodings.add(cert.getEncoded());
}
return encodings.toArray(new byte[encodings.size()][]);
}
/**
* Function called via JNI to get the private key the user selected.
*
* Since this method is called from a thread of charon's thread pool we are safe
* to call methods on KeyChain directly.
*
* @return the private key
* @throws InterruptedException
* @throws KeyChainException
* @throws CertificateEncodingException
*/
private PrivateKey getUserKey() throws KeyChainException, InterruptedException
{
return KeyChain.getPrivateKey(getApplicationContext(), mCurrentUserCertificateAlias);
}
/**
* Initialization of charon, provided by libandroidbridge.so
*
* @param builder BuilderAdapter for this connection
* @param logfile absolute path to the logfile
* @param boyd enable BYOD features
* @return TRUE if initialization was successful
*/
public native boolean initializeCharon(BuilderAdapter builder, String logfile, boolean byod);
/**
* Deinitialize charon, provided by libandroidbridge.so
*/
public native void deinitializeCharon();
/**
* Initiate VPN, provided by libandroidbridge.so
*/
//CMA
public native void initiate(String type, String gateway, String username, String password, String psk, String serverauth);
/**
* Adapter for VpnService.Builder which is used to access it safely via JNI.
* There is a corresponding C object to access it from native code.
*/
public class BuilderAdapter
{
private final String mName;
private VpnService.Builder mBuilder;
private BuilderCache mCache;
private BuilderCache mEstablishedCache;
public BuilderAdapter(String name)
{
mName = name;
mBuilder = createBuilder(name);
mCache = new BuilderCache();
}
private VpnService.Builder createBuilder(String name)
{
VpnService.Builder builder = new CharonVpnService.Builder();
builder.setSession(mName);
/* even though the option displayed in the system dialog says "Configure"
* we just use our main Activity */
Context context = getApplicationContext();
//CMA - use with our main activity
Intent intent = new Intent(context, DashboardActivity.class);
PendingIntent pending = PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
builder.setConfigureIntent(pending);
return builder;
}
public synchronized boolean addAddress(String address, int prefixLength)
{
try
{
mBuilder.addAddress(address, prefixLength);
mCache.addAddress(address, prefixLength);
}
catch (IllegalArgumentException ex)
{
return false;
}
return true;
}
public synchronized boolean addDnsServer(String address)
{
try
{
mBuilder.addDnsServer(address);
}
catch (IllegalArgumentException ex)
{
return false;
}
return true;
}
public synchronized boolean addRoute(String address, int prefixLength)
{
try
{
mBuilder.addRoute(address, prefixLength);
mCache.addRoute(address, prefixLength);
}
catch (IllegalArgumentException ex)
{
return false;
}
return true;
}
public synchronized boolean addSearchDomain(String domain)
{
try
{
mBuilder.addSearchDomain(domain);
}
catch (IllegalArgumentException ex)
{
return false;
}
return true;
}
public synchronized boolean setMtu(int mtu)
{
try
{
mBuilder.setMtu(mtu);
mCache.setMtu(mtu);
}
catch (IllegalArgumentException ex)
{
return false;
}
return true;
}
public synchronized int establish()
{
ParcelFileDescriptor fd;
try
{
fd = mBuilder.establish();
}
catch (Exception ex)
{
ex.printStackTrace();
return -1;
}
if (fd == null)
{
return -1;
}
/* now that the TUN device is created we don't need the current
* builder anymore, but we might need another when reestablishing */
mBuilder = createBuilder(mName);
mEstablishedCache = mCache;
mCache = new BuilderCache();
return fd.detachFd();
}
public synchronized int establishNoDns()
{
ParcelFileDescriptor fd;
if (mEstablishedCache == null)
{
return -1;
}
try
{
Builder builder = createBuilder(mName);
mEstablishedCache.applyData(builder);
fd = builder.establish();
}
catch (Exception ex)
{
ex.printStackTrace();
return -1;
}
if (fd == null)
{
return -1;
}
return fd.detachFd();
}
//CMA
public synchronized int reauthenticateWithPlumber() {
mPlumber = new PlumberReauthenticater();
registerCharonListener(mPlumber);
mPlumber.disconnect();
return 1;
}
}
/**
* Cache non DNS related information so we can recreate the builder without
* that information when reestablishing IKE_SAs
*/
public class BuilderCache
{
private final List<PrefixedAddress> mAddresses = new ArrayList<PrefixedAddress>();
private final List<PrefixedAddress> mRoutes = new ArrayList<PrefixedAddress>();
private int mMtu;
public void addAddress(String address, int prefixLength)
{
mAddresses.add(new PrefixedAddress(address, prefixLength));
}
public void addRoute(String address, int prefixLength)
{
mRoutes.add(new PrefixedAddress(address, prefixLength));
}
public void setMtu(int mtu)
{
mMtu = mtu;
}
public void applyData(VpnService.Builder builder)
{
for (PrefixedAddress address : mAddresses)
{
builder.addAddress(address.mAddress, address.mPrefix);
}
for (PrefixedAddress route : mRoutes)
{
builder.addRoute(route.mAddress, route.mPrefix);
}
builder.setMtu(mMtu);
}
private class PrefixedAddress
{
public String mAddress;
public int mPrefix;
public PrefixedAddress(String address, int prefix)
{
this.mAddress = address;
this.mPrefix = prefix;
}
}
}
/*
* The libraries are extracted to /data/data/org.strongswan.android/...
* during installation.
*/
static
{
System.loadLibrary("strongswan");
//CMA
if (Config.USE_BYOD)
{
System.loadLibrary("tncif");
System.loadLibrary("tnccs");
System.loadLibrary("imcv");
}
System.loadLibrary("hydra");
System.loadLibrary("charon");
System.loadLibrary("ipsec");
System.loadLibrary("androidbridge");
}
//CMA
class PlumberReauthenticater implements CharonListener {
public void disconnect(){
if (BuildConfig.DEBUG) Log.d(TAG, "reauthenticateWithPlumber() - disconnecting");
new DisconnectVPN().start();
}
public void reauthenticate(){
if (BuildConfig.DEBUG) Log.d(TAG, "reauthenticateWithPlumber() - reauthenticating");
new ReauthenticateWithPlumber().start();
}
@Override
public void charonStopped() {
reauthenticate();
unregisterCharonListener(mPlumber);
}
class DisconnectVPN extends Thread {
@Override
public void run() {
if (mService != null) {
mService.disconnect();
}
}
}
class ReauthenticateWithPlumber extends Thread {
@Override
public void run() {
//wait until we have internet connection again
int count = 0;
final int MSECONDS = 5000;
while (!connectivityAvailable(getApplicationContext())) {
//don't try forever
if (count > 15) {
if (BuildConfig.DEBUG) Log.d(TAG, "reauthenticateWithPlumber() - " +
"couldn't find any internet connection - don't start vpn...");
return;
}
if (BuildConfig.DEBUG) Log.d(TAG, "reauthenticateWithPlumber() - NO CONNECTION. WAIT!");
count++;
try {
//always increase wait time: 5,10,15,..75s ->
// 15/2 * (2*5 + (15-1)*5) = total 600s
Thread.sleep(MSECONDS * count);
} catch (InterruptedException e) {
}
}
//reauthenticate with our plumber
PlumberAuthResponse result = backendConnector.authenticateUserSync(
Config.getInstance().getPhoneNumber(), Config.getInstance().getDeviceId(),
Config.getInstance().getAuthToken());
if (result == null || result.getStatus() == PlumberStatus.FAILURE) {
if (BuildConfig.DEBUG) Log.d(TAG, "reauthenticateWithPlumber() -" +
" Error when parsing the result - don't start vpn...");
return;
}
//save new auth token for next use!
Config.getInstance().saveAuthToken(result.getUserToken());
VpnProfile prof = VpnConfigurator.getVpnProfile(result.getUsername(),
result.getPassword(), result.getPsk());
//reconnect with VPN
if (BuildConfig.DEBUG) Log.d(TAG, "reauthenticateWithPlumber() - reconnecting");
setNextProfile(prof);
mPlumber = null;
}
}
public boolean connectivityAvailable(Context context) {
ConnectivityManager connectivityManager = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = null;
if (connectivityManager != null) {
networkInfo = connectivityManager.getActiveNetworkInfo();
}
if (networkInfo == null || !networkInfo.isConnected()) {
return false;
}
Socket sock = new Socket();
try {
SocketAddress sockaddr = new InetSocketAddress(Config.PLUMBER_ADDR, 443); //for now
// Create an unbound socket
sock = new Socket();
// This method will block no more than timeoutMs.
// If the timeout occurs, SocketTimeoutException is thrown.
int timeoutMs = 2000; // 2 seconds
sock.connect(sockaddr, timeoutMs);
return true;
} catch(Exception e) {
return false;
} finally {
if (sock.isConnected()) {
try {
sock.close();
} catch (IOException e) {
if (BuildConfig.DEBUG) Log.e(Config.TAG, "error", e);
}
}
}
}
}
}