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