305ee80b2d463a96770a814141da45c12966f659
[strongswan.git] / src / frontends / android / src / org / strongswan / android / logic / CharonVpnService.java
1 /*
2 * Copyright (C) 2012-2013 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.io.File;
21 import java.security.PrivateKey;
22 import java.security.cert.CertificateEncodingException;
23 import java.security.cert.X509Certificate;
24 import java.util.ArrayList;
25
26 import org.strongswan.android.data.VpnProfile;
27 import org.strongswan.android.data.VpnProfileDataSource;
28 import org.strongswan.android.logic.VpnStateService.ErrorState;
29 import org.strongswan.android.logic.VpnStateService.State;
30 import org.strongswan.android.logic.imc.ImcState;
31 import org.strongswan.android.logic.imc.RemediationInstruction;
32 import org.strongswan.android.ui.MainActivity;
33
34 import android.app.PendingIntent;
35 import android.app.Service;
36 import android.content.ComponentName;
37 import android.content.Context;
38 import android.content.Intent;
39 import android.content.ServiceConnection;
40 import android.net.VpnService;
41 import android.os.Bundle;
42 import android.os.IBinder;
43 import android.os.ParcelFileDescriptor;
44 import android.security.KeyChain;
45 import android.security.KeyChainException;
46 import android.util.Log;
47
48 public class CharonVpnService extends VpnService implements Runnable
49 {
50 private static final String TAG = CharonVpnService.class.getSimpleName();
51 public static final String LOG_FILE = "charon.log";
52
53 private String mLogFile;
54 private VpnProfileDataSource mDataSource;
55 private Thread mConnectionHandler;
56 private VpnProfile mCurrentProfile;
57 private volatile String mCurrentCertificateAlias;
58 private volatile String mCurrentUserCertificateAlias;
59 private VpnProfile mNextProfile;
60 private volatile boolean mProfileUpdated;
61 private volatile boolean mTerminate;
62 private volatile boolean mIsDisconnecting;
63 private VpnStateService mService;
64 private final Object mServiceLock = new Object();
65 private final ServiceConnection mServiceConnection = new ServiceConnection() {
66 @Override
67 public void onServiceDisconnected(ComponentName name)
68 { /* since the service is local this is theoretically only called when the process is terminated */
69 synchronized (mServiceLock)
70 {
71 mService = null;
72 }
73 }
74
75 @Override
76 public void onServiceConnected(ComponentName name, IBinder service)
77 {
78 synchronized (mServiceLock)
79 {
80 mService = ((VpnStateService.LocalBinder)service).getService();
81 }
82 /* we are now ready to start the handler thread */
83 mConnectionHandler.start();
84 }
85 };
86
87 /**
88 * as defined in charonservice.h
89 */
90 static final int STATE_CHILD_SA_UP = 1;
91 static final int STATE_CHILD_SA_DOWN = 2;
92 static final int STATE_AUTH_ERROR = 3;
93 static final int STATE_PEER_AUTH_ERROR = 4;
94 static final int STATE_LOOKUP_ERROR = 5;
95 static final int STATE_UNREACHABLE_ERROR = 6;
96 static final int STATE_GENERIC_ERROR = 7;
97
98 @Override
99 public int onStartCommand(Intent intent, int flags, int startId)
100 {
101 if (intent != null)
102 {
103 Bundle bundle = intent.getExtras();
104 VpnProfile profile = null;
105 if (bundle != null)
106 {
107 profile = mDataSource.getVpnProfile(bundle.getLong(VpnProfileDataSource.KEY_ID));
108 if (profile != null)
109 {
110 String password = bundle.getString(VpnProfileDataSource.KEY_PASSWORD);
111 profile.setPassword(password);
112 }
113 }
114 setNextProfile(profile);
115 }
116 return START_NOT_STICKY;
117 }
118
119 @Override
120 public void onCreate()
121 {
122 mLogFile = getFilesDir().getAbsolutePath() + File.separator + LOG_FILE;
123
124 mDataSource = new VpnProfileDataSource(this);
125 mDataSource.open();
126 /* use a separate thread as main thread for charon */
127 mConnectionHandler = new Thread(this);
128 /* the thread is started when the service is bound */
129 bindService(new Intent(this, VpnStateService.class),
130 mServiceConnection, Service.BIND_AUTO_CREATE);
131 }
132
133 @Override
134 public void onRevoke()
135 { /* the system revoked the rights grated with the initial prepare() call.
136 * called when the user clicks disconnect in the system's VPN dialog */
137 setNextProfile(null);
138 }
139
140 @Override
141 public void onDestroy()
142 {
143 mTerminate = true;
144 setNextProfile(null);
145 try
146 {
147 mConnectionHandler.join();
148 }
149 catch (InterruptedException e)
150 {
151 e.printStackTrace();
152 }
153 if (mService != null)
154 {
155 unbindService(mServiceConnection);
156 }
157 mDataSource.close();
158 }
159
160 /**
161 * Set the profile that is to be initiated next. Notify the handler thread.
162 *
163 * @param profile the profile to initiate
164 */
165 private void setNextProfile(VpnProfile profile)
166 {
167 synchronized (this)
168 {
169 this.mNextProfile = profile;
170 mProfileUpdated = true;
171 notifyAll();
172 }
173 }
174
175 @Override
176 public void run()
177 {
178 while (true)
179 {
180 synchronized (this)
181 {
182 try
183 {
184 while (!mProfileUpdated)
185 {
186 wait();
187 }
188
189 mProfileUpdated = false;
190 stopCurrentConnection();
191 if (mNextProfile == null)
192 {
193 setProfile(null);
194 setState(State.DISABLED);
195 if (mTerminate)
196 {
197 break;
198 }
199 }
200 else
201 {
202 mCurrentProfile = mNextProfile;
203 mNextProfile = null;
204
205 /* store this in a separate (volatile) variable to avoid
206 * a possible deadlock during deinitialization */
207 mCurrentCertificateAlias = mCurrentProfile.getCertificateAlias();
208 mCurrentUserCertificateAlias = mCurrentProfile.getUserCertificateAlias();
209
210 setProfile(mCurrentProfile);
211 setError(ErrorState.NO_ERROR);
212 setState(State.CONNECTING);
213 setImcState(ImcState.UNKNOWN);
214 mIsDisconnecting = false;
215
216 BuilderAdapter builder = new BuilderAdapter(mCurrentProfile.getName());
217 if (initializeCharon(builder, mLogFile, mCurrentProfile.getVpnType().getEnableBYOD()))
218 {
219 Log.i(TAG, "charon started");
220 initiate(mCurrentProfile.getVpnType().getIdentifier(),
221 mCurrentProfile.getGateway(), mCurrentProfile.getUsername(),
222 mCurrentProfile.getPassword());
223 }
224 else
225 {
226 Log.e(TAG, "failed to start charon");
227 setError(ErrorState.GENERIC_ERROR);
228 setState(State.DISABLED);
229 mCurrentProfile = null;
230 }
231 }
232 }
233 catch (InterruptedException ex)
234 {
235 stopCurrentConnection();
236 setState(State.DISABLED);
237 }
238 }
239 }
240 }
241
242 /**
243 * Stop any existing connection by deinitializing charon.
244 */
245 private void stopCurrentConnection()
246 {
247 synchronized (this)
248 {
249 if (mCurrentProfile != null)
250 {
251 setState(State.DISCONNECTING);
252 mIsDisconnecting = true;
253 deinitializeCharon();
254 Log.i(TAG, "charon stopped");
255 mCurrentProfile = null;
256 }
257 }
258 }
259
260 /**
261 * Update the VPN profile on the state service. Called by the handler thread.
262 *
263 * @param profile currently active VPN profile
264 */
265 private void setProfile(VpnProfile profile)
266 {
267 synchronized (mServiceLock)
268 {
269 if (mService != null)
270 {
271 mService.setProfile(profile);
272 }
273 }
274 }
275
276 /**
277 * Update the current VPN state on the state service. Called by the handler
278 * thread and any of charon's threads.
279 *
280 * @param state current state
281 */
282 private void setState(State state)
283 {
284 synchronized (mServiceLock)
285 {
286 if (mService != null)
287 {
288 mService.setState(state);
289 }
290 }
291 }
292
293 /**
294 * Set an error on the state service. Called by the handler thread and any
295 * of charon's threads.
296 *
297 * @param error error state
298 */
299 private void setError(ErrorState error)
300 {
301 synchronized (mServiceLock)
302 {
303 if (mService != null)
304 {
305 mService.setError(error);
306 }
307 }
308 }
309
310 /**
311 * Set the IMC state on the state service. Called by the handler thread and
312 * any of charon's threads.
313 *
314 * @param state IMC state
315 */
316 private void setImcState(ImcState state)
317 {
318 synchronized (mServiceLock)
319 {
320 if (mService != null)
321 {
322 mService.setImcState(state);
323 }
324 }
325 }
326
327 /**
328 * Set an error on the state service and disconnect the current connection.
329 * This is not done by calling stopCurrentConnection() above, but instead
330 * is done asynchronously via state service.
331 *
332 * @param error error state
333 */
334 private void setErrorDisconnect(ErrorState error)
335 {
336 synchronized (mServiceLock)
337 {
338 if (mService != null)
339 {
340 mService.setError(error);
341 if (!mIsDisconnecting)
342 {
343 mService.disconnect();
344 }
345 }
346 }
347 }
348
349 /**
350 * Updates the state of the current connection.
351 * Called via JNI by different threads (but not concurrently).
352 *
353 * @param status new state
354 */
355 public void updateStatus(int status)
356 {
357 switch (status)
358 {
359 case STATE_CHILD_SA_DOWN:
360 /* we ignore this as we use closeaction=restart */
361 break;
362 case STATE_CHILD_SA_UP:
363 setState(State.CONNECTED);
364 break;
365 case STATE_AUTH_ERROR:
366 setErrorDisconnect(ErrorState.AUTH_FAILED);
367 break;
368 case STATE_PEER_AUTH_ERROR:
369 setErrorDisconnect(ErrorState.PEER_AUTH_FAILED);
370 break;
371 case STATE_LOOKUP_ERROR:
372 setErrorDisconnect(ErrorState.LOOKUP_FAILED);
373 break;
374 case STATE_UNREACHABLE_ERROR:
375 setErrorDisconnect(ErrorState.UNREACHABLE);
376 break;
377 case STATE_GENERIC_ERROR:
378 setErrorDisconnect(ErrorState.GENERIC_ERROR);
379 break;
380 default:
381 Log.e(TAG, "Unknown status code received");
382 break;
383 }
384 }
385
386 /**
387 * Updates the IMC state of the current connection.
388 * Called via JNI by different threads (but not concurrently).
389 *
390 * @param value new state
391 */
392 public void updateImcState(int value)
393 {
394 ImcState state = ImcState.fromValue(value);
395 if (state != null)
396 {
397 setImcState(state);
398 }
399 }
400
401 /**
402 * Add a remediation instruction to the VPN state service.
403 * Called via JNI by different threads (but not concurrently).
404 *
405 * @param xml XML text
406 */
407 public void addRemediationInstruction(String xml)
408 {
409 for (RemediationInstruction instruction : RemediationInstruction.fromXml(xml))
410 {
411 synchronized (mServiceLock)
412 {
413 if (mService != null)
414 {
415 mService.addRemediationInstruction(instruction);
416 }
417 }
418 }
419 }
420
421 /**
422 * Function called via JNI to generate a list of DER encoded CA certificates
423 * as byte array.
424 *
425 * @param hash optional alias (only hash part), if given matching certificates are returned
426 * @return a list of DER encoded CA certificates
427 */
428 private byte[][] getTrustedCertificates(String hash)
429 {
430 ArrayList<byte[]> certs = new ArrayList<byte[]>();
431 TrustedCertificateManager certman = TrustedCertificateManager.getInstance();
432 try
433 {
434 if (hash != null)
435 {
436 String alias = "user:" + hash + ".0";
437 X509Certificate cert = certman.getCACertificateFromAlias(alias);
438 if (cert == null)
439 {
440 alias = "system:" + hash + ".0";
441 cert = certman.getCACertificateFromAlias(alias);
442 }
443 if (cert == null)
444 {
445 return null;
446 }
447 certs.add(cert.getEncoded());
448 }
449 else
450 {
451 String alias = this.mCurrentCertificateAlias;
452 if (alias != null)
453 {
454 X509Certificate cert = certman.getCACertificateFromAlias(alias);
455 if (cert == null)
456 {
457 return null;
458 }
459 certs.add(cert.getEncoded());
460 }
461 else
462 {
463 for (X509Certificate cert : certman.getAllCACertificates().values())
464 {
465 certs.add(cert.getEncoded());
466 }
467 }
468 }
469 }
470 catch (CertificateEncodingException e)
471 {
472 e.printStackTrace();
473 return null;
474 }
475 return certs.toArray(new byte[certs.size()][]);
476 }
477
478 /**
479 * Function called via JNI to get a list containing the DER encoded certificates
480 * of the user selected certificate chain (beginning with the user certificate).
481 *
482 * Since this method is called from a thread of charon's thread pool we are safe
483 * to call methods on KeyChain directly.
484 *
485 * @return list containing the certificates (first element is the user certificate)
486 * @throws InterruptedException
487 * @throws KeyChainException
488 * @throws CertificateEncodingException
489 */
490 private byte[][] getUserCertificate() throws KeyChainException, InterruptedException, CertificateEncodingException
491 {
492 ArrayList<byte[]> encodings = new ArrayList<byte[]>();
493 X509Certificate[] chain = KeyChain.getCertificateChain(getApplicationContext(), mCurrentUserCertificateAlias);
494 if (chain == null || chain.length == 0)
495 {
496 return null;
497 }
498 for (X509Certificate cert : chain)
499 {
500 encodings.add(cert.getEncoded());
501 }
502 return encodings.toArray(new byte[encodings.size()][]);
503 }
504
505 /**
506 * Function called via JNI to get the private key the user selected.
507 *
508 * Since this method is called from a thread of charon's thread pool we are safe
509 * to call methods on KeyChain directly.
510 *
511 * @return the private key
512 * @throws InterruptedException
513 * @throws KeyChainException
514 * @throws CertificateEncodingException
515 */
516 private PrivateKey getUserKey() throws KeyChainException, InterruptedException
517 {
518 return KeyChain.getPrivateKey(getApplicationContext(), mCurrentUserCertificateAlias);
519
520 }
521
522 /**
523 * Initialization of charon, provided by libandroidbridge.so
524 *
525 * @param builder BuilderAdapter for this connection
526 * @param logfile absolute path to the logfile
527 * @param boyd enable BYOD features
528 * @return TRUE if initialization was successful
529 */
530 public native boolean initializeCharon(BuilderAdapter builder, String logfile, boolean byod);
531
532 /**
533 * Deinitialize charon, provided by libandroidbridge.so
534 */
535 public native void deinitializeCharon();
536
537 /**
538 * Initiate VPN, provided by libandroidbridge.so
539 */
540 public native void initiate(String type, String gateway, String username, String password);
541
542 /**
543 * Adapter for VpnService.Builder which is used to access it safely via JNI.
544 * There is a corresponding C object to access it from native code.
545 */
546 public class BuilderAdapter
547 {
548 private final String mName;
549 private VpnService.Builder mBuilder;
550
551 public BuilderAdapter(String name)
552 {
553 mName = name;
554 mBuilder = createBuilder(name);
555 }
556
557 private VpnService.Builder createBuilder(String name)
558 {
559 VpnService.Builder builder = new CharonVpnService.Builder();
560 builder.setSession(mName);
561
562 /* even though the option displayed in the system dialog says "Configure"
563 * we just use our main Activity */
564 Context context = getApplicationContext();
565 Intent intent = new Intent(context, MainActivity.class);
566 PendingIntent pending = PendingIntent.getActivity(context, 0, intent,
567 PendingIntent.FLAG_UPDATE_CURRENT);
568 builder.setConfigureIntent(pending);
569 return builder;
570 }
571
572 public synchronized boolean addAddress(String address, int prefixLength)
573 {
574 try
575 {
576 mBuilder.addAddress(address, prefixLength);
577 }
578 catch (IllegalArgumentException ex)
579 {
580 return false;
581 }
582 return true;
583 }
584
585 public synchronized boolean addDnsServer(String address)
586 {
587 try
588 {
589 mBuilder.addDnsServer(address);
590 }
591 catch (IllegalArgumentException ex)
592 {
593 return false;
594 }
595 return true;
596 }
597
598 public synchronized boolean addRoute(String address, int prefixLength)
599 {
600 try
601 {
602 mBuilder.addRoute(address, prefixLength);
603 }
604 catch (IllegalArgumentException ex)
605 {
606 return false;
607 }
608 return true;
609 }
610
611 public synchronized boolean addSearchDomain(String domain)
612 {
613 try
614 {
615 mBuilder.addSearchDomain(domain);
616 }
617 catch (IllegalArgumentException ex)
618 {
619 return false;
620 }
621 return true;
622 }
623
624 public synchronized boolean setMtu(int mtu)
625 {
626 try
627 {
628 mBuilder.setMtu(mtu);
629 }
630 catch (IllegalArgumentException ex)
631 {
632 return false;
633 }
634 return true;
635 }
636
637 public synchronized int establish()
638 {
639 ParcelFileDescriptor fd;
640 try
641 {
642 fd = mBuilder.establish();
643 }
644 catch (Exception ex)
645 {
646 ex.printStackTrace();
647 return -1;
648 }
649 if (fd == null)
650 {
651 return -1;
652 }
653 /* now that the TUN device is created we don't need the current
654 * builder anymore, but we might need another when reestablishing */
655 mBuilder = createBuilder(mName);
656 return fd.detachFd();
657 }
658 }
659
660 /*
661 * The libraries are extracted to /data/data/org.strongswan.android/...
662 * during installation.
663 */
664 static
665 {
666 System.loadLibrary("crypto");
667 System.loadLibrary("strongswan");
668
669 if (MainActivity.USE_BYOD)
670 {
671 System.loadLibrary("tncif");
672 System.loadLibrary("tnccs");
673 System.loadLibrary("imcv");
674 System.loadLibrary("pts");
675 }
676
677 System.loadLibrary("hydra");
678 System.loadLibrary("charon");
679 System.loadLibrary("ipsec");
680 System.loadLibrary("androidbridge");
681 }
682 }