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