Initiate an SA via native JNI method
[strongswan.git] / src / frontends / android / src / org / strongswan / android / logic / CharonVpnService.java
1 /*
2 * Copyright (C) 2012 Tobias Brunner
3 * Copyright (C) 2012 Giuliano Grassi
4 * Copyright (C) 2012 Ralf Sager
5 * Hochschule fuer Technik Rapperswil
6 *
7 * This program is free software; you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation; either version 2 of the License, or (at your
10 * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 * for more details.
16 */
17
18 package org.strongswan.android.logic;
19
20 import java.net.InetAddress;
21 import java.net.NetworkInterface;
22 import java.net.SocketException;
23 import java.security.cert.CertificateEncodingException;
24 import java.security.cert.X509Certificate;
25 import java.util.ArrayList;
26 import java.util.Enumeration;
27
28 import org.strongswan.android.data.VpnProfile;
29 import org.strongswan.android.data.VpnProfileDataSource;
30 import org.strongswan.android.logic.VpnStateService.ErrorState;
31 import org.strongswan.android.logic.VpnStateService.State;
32
33 import android.app.Service;
34 import android.content.ComponentName;
35 import android.content.Intent;
36 import android.content.ServiceConnection;
37 import android.net.VpnService;
38 import android.os.Bundle;
39 import android.os.IBinder;
40 import android.util.Log;
41
42 public class CharonVpnService extends VpnService implements Runnable
43 {
44 private static final String TAG = CharonVpnService.class.getSimpleName();
45 private VpnProfileDataSource mDataSource;
46 private Thread mConnectionHandler;
47 private VpnProfile mCurrentProfile;
48 private VpnProfile mNextProfile;
49 private volatile boolean mProfileUpdated;
50 private volatile boolean mTerminate;
51 private VpnStateService mService;
52 private final Object mServiceLock = new Object();
53 private final ServiceConnection mServiceConnection = new ServiceConnection() {
54 @Override
55 public void onServiceDisconnected(ComponentName name)
56 { /* since the service is local this is theoretically only called when the process is terminated */
57 synchronized (mServiceLock)
58 {
59 mService = null;
60 }
61 }
62
63 @Override
64 public void onServiceConnected(ComponentName name, IBinder service)
65 {
66 synchronized (mServiceLock)
67 {
68 mService = ((VpnStateService.LocalBinder)service).getService();
69 }
70 /* we are now ready to start the handler thread */
71 mConnectionHandler.start();
72 }
73 };
74
75 /**
76 * as defined in charonservice.h
77 */
78 static final int STATE_CHILD_SA_UP = 1;
79 static final int STATE_CHILD_SA_DOWN = 2;
80 static final int STATE_AUTH_ERROR = 3;
81 static final int STATE_PEER_AUTH_ERROR = 4;
82 static final int STATE_LOOKUP_ERROR = 5;
83 static final int STATE_UNREACHABLE_ERROR = 6;
84 static final int STATE_GENERIC_ERROR = 7;
85
86 @Override
87 public int onStartCommand(Intent intent, int flags, int startId)
88 {
89 if (intent != null)
90 {
91 Bundle bundle = intent.getExtras();
92 VpnProfile profile = null;
93 if (bundle != null)
94 {
95 profile = mDataSource.getVpnProfile(bundle.getLong(VpnProfileDataSource.KEY_ID));
96 if (profile != null)
97 {
98 String password = bundle.getString(VpnProfileDataSource.KEY_PASSWORD);
99 profile.setPassword(password);
100 }
101 }
102 setNextProfile(profile);
103 }
104 return START_NOT_STICKY;
105 }
106
107 @Override
108 public void onCreate()
109 {
110 mDataSource = new VpnProfileDataSource(this);
111 mDataSource.open();
112 /* use a separate thread as main thread for charon */
113 mConnectionHandler = new Thread(this);
114 /* the thread is started when the service is bound */
115 bindService(new Intent(this, VpnStateService.class),
116 mServiceConnection, Service.BIND_AUTO_CREATE);
117 }
118
119 @Override
120 public void onRevoke()
121 { /* the system revoked the rights grated with the initial prepare() call.
122 * called when the user clicks disconnect in the system's VPN dialog */
123 setNextProfile(null);
124 }
125
126 @Override
127 public void onDestroy()
128 {
129 mTerminate = true;
130 setNextProfile(null);
131 try
132 {
133 mConnectionHandler.join();
134 }
135 catch (InterruptedException e)
136 {
137 e.printStackTrace();
138 }
139 if (mService != null)
140 {
141 unbindService(mServiceConnection);
142 }
143 mDataSource.close();
144 }
145
146 /**
147 * Set the profile that is to be initiated next. Notify the handler thread.
148 *
149 * @param profile the profile to initiate
150 */
151 private void setNextProfile(VpnProfile profile)
152 {
153 synchronized (this)
154 {
155 this.mNextProfile = profile;
156 mProfileUpdated = true;
157 notifyAll();
158 }
159 }
160
161 @Override
162 public void run()
163 {
164 while (true)
165 {
166 synchronized (this)
167 {
168 try
169 {
170 while (!mProfileUpdated)
171 {
172 wait();
173 }
174
175 mProfileUpdated = false;
176 stopCurrentConnection();
177 if (mNextProfile == null)
178 {
179 setProfile(null);
180 setState(State.DISABLED);
181 if (mTerminate)
182 {
183 break;
184 }
185 }
186 else
187 {
188 mCurrentProfile = mNextProfile;
189 mNextProfile = null;
190
191 setProfile(mCurrentProfile);
192 setError(ErrorState.NO_ERROR);
193 setState(State.CONNECTING);
194
195 initializeCharon();
196 Log.i(TAG, "charon started");
197
198 String local_address = getLocalIPv4Address();
199 initiate(local_address != null ? local_address : "0.0.0.0",
200 mCurrentProfile.getGateway(), mCurrentProfile.getUsername(),
201 mCurrentProfile.getPassword());
202 }
203 }
204 catch (InterruptedException ex)
205 {
206 stopCurrentConnection();
207 setState(State.DISABLED);
208 }
209 }
210 }
211 }
212
213 /**
214 * Stop any existing connection by deinitializing charon.
215 */
216 private void stopCurrentConnection()
217 {
218 synchronized (this)
219 {
220 if (mCurrentProfile != null)
221 {
222 setState(State.DISCONNECTING);
223 deinitializeCharon();
224 Log.i(TAG, "charon stopped");
225 mCurrentProfile = null;
226 }
227 }
228 }
229
230 /**
231 * Update the VPN profile on the state service. Called by the handler thread.
232 *
233 * @param profile currently active VPN profile
234 */
235 private void setProfile(VpnProfile profile)
236 {
237 synchronized (mServiceLock)
238 {
239 if (mService != null)
240 {
241 mService.setProfile(profile);
242 }
243 }
244 }
245
246 /**
247 * Update the current VPN state on the state service. Called by the handler
248 * thread and any of charon's threads.
249 *
250 * @param state current state
251 */
252 private void setState(State state)
253 {
254 synchronized (mServiceLock)
255 {
256 if (mService != null)
257 {
258 mService.setState(state);
259 }
260 }
261 }
262
263 /**
264 * Set an error on the state service. Called by the handler thread and any
265 * of charon's threads.
266 *
267 * @param error error state
268 */
269 private void setError(ErrorState error)
270 {
271 synchronized (mServiceLock)
272 {
273 if (mService != null)
274 {
275 mService.setError(error);
276 }
277 }
278 }
279
280 /**
281 * Set an error on the state service and disconnect the current connection.
282 * This is not done by calling stopCurrentConnection() above, but instead
283 * is done asynchronously via state service.
284 *
285 * @param error error state
286 */
287 private void setErrorDisconnect(ErrorState error)
288 {
289 synchronized (mServiceLock)
290 {
291 if (mService != null)
292 {
293 mService.setError(error);
294 mService.disconnect();
295 }
296 }
297 }
298
299 /**
300 * Updates the state of the current connection.
301 * Called via JNI by different threads (but not concurrently).
302 *
303 * @param status new state
304 */
305 public void updateStatus(int status)
306 {
307 switch (status)
308 {
309 case STATE_CHILD_SA_DOWN:
310 synchronized (mServiceLock)
311 {
312 /* since this state is also reached when the SA is closed remotely,
313 * we call disconnect() to make sure charon is properly deinitialized */
314 if (mService != null)
315 {
316 mService.disconnect();
317 }
318 }
319 break;
320 case STATE_CHILD_SA_UP:
321 setState(State.CONNECTED);
322 break;
323 case STATE_AUTH_ERROR:
324 setErrorDisconnect(ErrorState.AUTH_FAILED);
325 break;
326 case STATE_PEER_AUTH_ERROR:
327 setErrorDisconnect(ErrorState.PEER_AUTH_FAILED);
328 break;
329 case STATE_LOOKUP_ERROR:
330 setErrorDisconnect(ErrorState.LOOKUP_FAILED);
331 break;
332 case STATE_UNREACHABLE_ERROR:
333 setErrorDisconnect(ErrorState.UNREACHABLE);
334 break;
335 case STATE_GENERIC_ERROR:
336 setErrorDisconnect(ErrorState.GENERIC_ERROR);
337 break;
338 default:
339 Log.e(TAG, "Unknown status code received");
340 break;
341 }
342 }
343
344 /**
345 * Function called via JNI to generate a list of DER encoded CA certificates
346 * as byte array.
347 *
348 * @param hash optional alias (only hash part), if given matching certificates are returned
349 * @return a list of DER encoded CA certificates
350 */
351 private synchronized byte[][] getTrustedCertificates(String hash)
352 {
353 ArrayList<byte[]> certs = new ArrayList<byte[]>();
354 TrustedCertificateManager certman = TrustedCertificateManager.getInstance();
355 try
356 {
357 if (hash != null)
358 {
359 String alias = "user:" + hash + ".0";
360 X509Certificate cert = certman.getCACertificateFromAlias(alias);
361 if (cert == null)
362 {
363 alias = "system:" + hash + ".0";
364 cert = certman.getCACertificateFromAlias(alias);
365 }
366 if (cert == null)
367 {
368 return null;
369 }
370 certs.add(cert.getEncoded());
371 }
372 else
373 {
374 String alias = this.mCurrentProfile.getCertificateAlias();
375 if (alias != null)
376 {
377 X509Certificate cert = certman.getCACertificateFromAlias(alias);
378 if (cert == null)
379 {
380 return null;
381 }
382 certs.add(cert.getEncoded());
383 }
384 else
385 {
386 for (X509Certificate cert : certman.getAllCACertificates().values())
387 {
388 certs.add(cert.getEncoded());
389 }
390 }
391 }
392 }
393 catch (CertificateEncodingException e)
394 {
395 e.printStackTrace();
396 return null;
397 }
398 return certs.toArray(new byte[certs.size()][]);
399 }
400
401 /**
402 * Initialization of charon, provided by libandroidbridge.so
403 */
404 public native void initializeCharon();
405
406 /**
407 * Deinitialize charon, provided by libandroidbridge.so
408 */
409 public native void deinitializeCharon();
410
411 /**
412 * Initiate VPN, provided by libandroidbridge.so
413 */
414 public native void initiate(String local_address, String gateway,
415 String username, String password);
416
417 /**
418 * Helper function that retrieves a local IPv4 address.
419 *
420 * @return string representation of an IPv4 address, or null if none found
421 */
422 private static String getLocalIPv4Address()
423 {
424 try
425 {
426 Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces();
427 while (en.hasMoreElements())
428 {
429 NetworkInterface intf = en.nextElement();
430
431 Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses();
432 while (enumIpAddr.hasMoreElements())
433 {
434 InetAddress inetAddress = enumIpAddr.nextElement();
435 if (!inetAddress.isLoopbackAddress() && inetAddress.getAddress().length == 4)
436 {
437 return inetAddress.getHostAddress().toString();
438 }
439 }
440 }
441 }
442 catch (SocketException ex)
443 {
444 ex.printStackTrace();
445 return null;
446 }
447 return null;
448 }
449
450 /*
451 * The libraries are extracted to /data/data/org.strongswan.android/...
452 * during installation.
453 */
454 static
455 {
456 System.loadLibrary("crypto");
457 System.loadLibrary("strongswan");
458 System.loadLibrary("hydra");
459 System.loadLibrary("charon");
460 System.loadLibrary("ipsec");
461 System.loadLibrary("androidbridge");
462 }
463 }