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