android: Set preferred language for remediation instructions
[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("connection.type", mCurrentProfile.getVpnType().getIdentifier());
223 writer.setValue("connection.server", mCurrentProfile.getGateway());
224 writer.setValue("connection.username", mCurrentProfile.getUsername());
225 writer.setValue("connection.password", mCurrentProfile.getPassword());
226 initiate(writer.serialize());
227 }
228 else
229 {
230 Log.e(TAG, "failed to start charon");
231 setError(ErrorState.GENERIC_ERROR);
232 setState(State.DISABLED);
233 mCurrentProfile = null;
234 }
235 }
236 }
237 catch (InterruptedException ex)
238 {
239 stopCurrentConnection();
240 setState(State.DISABLED);
241 }
242 }
243 }
244 }
245
246 /**
247 * Stop any existing connection by deinitializing charon.
248 */
249 private void stopCurrentConnection()
250 {
251 synchronized (this)
252 {
253 if (mCurrentProfile != null)
254 {
255 setState(State.DISCONNECTING);
256 mIsDisconnecting = true;
257 deinitializeCharon();
258 Log.i(TAG, "charon stopped");
259 mCurrentProfile = null;
260 }
261 }
262 }
263
264 /**
265 * Notify the state service about a new connection attempt.
266 * Called by the handler thread.
267 *
268 * @param profile currently active VPN profile
269 */
270 private void startConnection(VpnProfile profile)
271 {
272 synchronized (mServiceLock)
273 {
274 if (mService != null)
275 {
276 mService.startConnection(profile);
277 }
278 }
279 }
280
281 /**
282 * Update the current VPN state on the state service. Called by the handler
283 * thread and any of charon's threads.
284 *
285 * @param state current state
286 */
287 private void setState(State state)
288 {
289 synchronized (mServiceLock)
290 {
291 if (mService != null)
292 {
293 mService.setState(state);
294 }
295 }
296 }
297
298 /**
299 * Set an error on the state service. Called by the handler thread and any
300 * of charon's threads.
301 *
302 * @param error error state
303 */
304 private void setError(ErrorState error)
305 {
306 synchronized (mServiceLock)
307 {
308 if (mService != null)
309 {
310 mService.setError(error);
311 }
312 }
313 }
314
315 /**
316 * Set the IMC state on the state service. Called by the handler thread and
317 * any of charon's threads.
318 *
319 * @param state IMC state
320 */
321 private void setImcState(ImcState state)
322 {
323 synchronized (mServiceLock)
324 {
325 if (mService != null)
326 {
327 mService.setImcState(state);
328 }
329 }
330 }
331
332 /**
333 * Set an error on the state service. Called by the handler thread and any
334 * of charon's threads.
335 *
336 * @param error error state
337 */
338 private void setErrorDisconnect(ErrorState error)
339 {
340 synchronized (mServiceLock)
341 {
342 if (mService != null)
343 {
344 if (!mIsDisconnecting)
345 {
346 mService.setError(error);
347 }
348 }
349 }
350 }
351
352 /**
353 * Updates the state of the current connection.
354 * Called via JNI by different threads (but not concurrently).
355 *
356 * @param status new state
357 */
358 public void updateStatus(int status)
359 {
360 switch (status)
361 {
362 case STATE_CHILD_SA_DOWN:
363 if (!mIsDisconnecting)
364 {
365 setState(State.CONNECTING);
366 }
367 break;
368 case STATE_CHILD_SA_UP:
369 setState(State.CONNECTED);
370 break;
371 case STATE_AUTH_ERROR:
372 setErrorDisconnect(ErrorState.AUTH_FAILED);
373 break;
374 case STATE_PEER_AUTH_ERROR:
375 setErrorDisconnect(ErrorState.PEER_AUTH_FAILED);
376 break;
377 case STATE_LOOKUP_ERROR:
378 setErrorDisconnect(ErrorState.LOOKUP_FAILED);
379 break;
380 case STATE_UNREACHABLE_ERROR:
381 setErrorDisconnect(ErrorState.UNREACHABLE);
382 break;
383 case STATE_GENERIC_ERROR:
384 setErrorDisconnect(ErrorState.GENERIC_ERROR);
385 break;
386 default:
387 Log.e(TAG, "Unknown status code received");
388 break;
389 }
390 }
391
392 /**
393 * Updates the IMC state of the current connection.
394 * Called via JNI by different threads (but not concurrently).
395 *
396 * @param value new state
397 */
398 public void updateImcState(int value)
399 {
400 ImcState state = ImcState.fromValue(value);
401 if (state != null)
402 {
403 setImcState(state);
404 }
405 }
406
407 /**
408 * Add a remediation instruction to the VPN state service.
409 * Called via JNI by different threads (but not concurrently).
410 *
411 * @param xml XML text
412 */
413 public void addRemediationInstruction(String xml)
414 {
415 for (RemediationInstruction instruction : RemediationInstruction.fromXml(xml))
416 {
417 synchronized (mServiceLock)
418 {
419 if (mService != null)
420 {
421 mService.addRemediationInstruction(instruction);
422 }
423 }
424 }
425 }
426
427 /**
428 * Function called via JNI to generate a list of DER encoded CA certificates
429 * as byte array.
430 *
431 * @return a list of DER encoded CA certificates
432 */
433 private byte[][] getTrustedCertificates()
434 {
435 ArrayList<byte[]> certs = new ArrayList<byte[]>();
436 TrustedCertificateManager certman = TrustedCertificateManager.getInstance();
437 try
438 {
439 String alias = this.mCurrentCertificateAlias;
440 if (alias != null)
441 {
442 X509Certificate cert = certman.getCACertificateFromAlias(alias);
443 if (cert == null)
444 {
445 return null;
446 }
447 certs.add(cert.getEncoded());
448 }
449 else
450 {
451 for (X509Certificate cert : certman.getAllCACertificates().values())
452 {
453 certs.add(cert.getEncoded());
454 }
455 }
456 }
457 catch (CertificateEncodingException e)
458 {
459 e.printStackTrace();
460 return null;
461 }
462 return certs.toArray(new byte[certs.size()][]);
463 }
464
465 /**
466 * Function called via JNI to get a list containing the DER encoded certificates
467 * of the user selected certificate chain (beginning with the user certificate).
468 *
469 * Since this method is called from a thread of charon's thread pool we are safe
470 * to call methods on KeyChain directly.
471 *
472 * @return list containing the certificates (first element is the user certificate)
473 * @throws InterruptedException
474 * @throws KeyChainException
475 * @throws CertificateEncodingException
476 */
477 private byte[][] getUserCertificate() throws KeyChainException, InterruptedException, CertificateEncodingException
478 {
479 ArrayList<byte[]> encodings = new ArrayList<byte[]>();
480 X509Certificate[] chain = KeyChain.getCertificateChain(getApplicationContext(), mCurrentUserCertificateAlias);
481 if (chain == null || chain.length == 0)
482 {
483 return null;
484 }
485 for (X509Certificate cert : chain)
486 {
487 encodings.add(cert.getEncoded());
488 }
489 return encodings.toArray(new byte[encodings.size()][]);
490 }
491
492 /**
493 * Function called via JNI to get the private key the user selected.
494 *
495 * Since this method is called from a thread of charon's thread pool we are safe
496 * to call methods on KeyChain directly.
497 *
498 * @return the private key
499 * @throws InterruptedException
500 * @throws KeyChainException
501 * @throws CertificateEncodingException
502 */
503 private PrivateKey getUserKey() throws KeyChainException, InterruptedException
504 {
505 return KeyChain.getPrivateKey(getApplicationContext(), mCurrentUserCertificateAlias);
506 }
507
508 /**
509 * Initialization of charon, provided by libandroidbridge.so
510 *
511 * @param builder BuilderAdapter for this connection
512 * @param logfile absolute path to the logfile
513 * @param boyd enable BYOD features
514 * @return TRUE if initialization was successful
515 */
516 public native boolean initializeCharon(BuilderAdapter builder, String logfile, boolean byod);
517
518 /**
519 * Deinitialize charon, provided by libandroidbridge.so
520 */
521 public native void deinitializeCharon();
522
523 /**
524 * Initiate VPN, provided by libandroidbridge.so
525 */
526 public native void initiate(String config);
527
528 /**
529 * Adapter for VpnService.Builder which is used to access it safely via JNI.
530 * There is a corresponding C object to access it from native code.
531 */
532 public class BuilderAdapter
533 {
534 private final String mName;
535 private VpnService.Builder mBuilder;
536 private BuilderCache mCache;
537 private BuilderCache mEstablishedCache;
538
539 public BuilderAdapter(String name)
540 {
541 mName = name;
542 mBuilder = createBuilder(name);
543 mCache = new BuilderCache();
544 }
545
546 private VpnService.Builder createBuilder(String name)
547 {
548 VpnService.Builder builder = new CharonVpnService.Builder();
549 builder.setSession(mName);
550
551 /* even though the option displayed in the system dialog says "Configure"
552 * we just use our main Activity */
553 Context context = getApplicationContext();
554 Intent intent = new Intent(context, MainActivity.class);
555 PendingIntent pending = PendingIntent.getActivity(context, 0, intent,
556 PendingIntent.FLAG_UPDATE_CURRENT);
557 builder.setConfigureIntent(pending);
558 return builder;
559 }
560
561 public synchronized boolean addAddress(String address, int prefixLength)
562 {
563 try
564 {
565 mBuilder.addAddress(address, prefixLength);
566 mCache.addAddress(address, prefixLength);
567 }
568 catch (IllegalArgumentException ex)
569 {
570 return false;
571 }
572 return true;
573 }
574
575 public synchronized boolean addDnsServer(String address)
576 {
577 try
578 {
579 mBuilder.addDnsServer(address);
580 }
581 catch (IllegalArgumentException ex)
582 {
583 return false;
584 }
585 return true;
586 }
587
588 public synchronized boolean addRoute(String address, int prefixLength)
589 {
590 try
591 {
592 mBuilder.addRoute(address, prefixLength);
593 mCache.addRoute(address, prefixLength);
594 }
595 catch (IllegalArgumentException ex)
596 {
597 return false;
598 }
599 return true;
600 }
601
602 public synchronized boolean addSearchDomain(String domain)
603 {
604 try
605 {
606 mBuilder.addSearchDomain(domain);
607 }
608 catch (IllegalArgumentException ex)
609 {
610 return false;
611 }
612 return true;
613 }
614
615 public synchronized boolean setMtu(int mtu)
616 {
617 try
618 {
619 mBuilder.setMtu(mtu);
620 mCache.setMtu(mtu);
621 }
622 catch (IllegalArgumentException ex)
623 {
624 return false;
625 }
626 return true;
627 }
628
629 public synchronized int establish()
630 {
631 ParcelFileDescriptor fd;
632 try
633 {
634 fd = mBuilder.establish();
635 }
636 catch (Exception ex)
637 {
638 ex.printStackTrace();
639 return -1;
640 }
641 if (fd == null)
642 {
643 return -1;
644 }
645 /* now that the TUN device is created we don't need the current
646 * builder anymore, but we might need another when reestablishing */
647 mBuilder = createBuilder(mName);
648 mEstablishedCache = mCache;
649 mCache = new BuilderCache();
650 return fd.detachFd();
651 }
652
653 public synchronized int establishNoDns()
654 {
655 ParcelFileDescriptor fd;
656
657 if (mEstablishedCache == null)
658 {
659 return -1;
660 }
661 try
662 {
663 Builder builder = createBuilder(mName);
664 mEstablishedCache.applyData(builder);
665 fd = builder.establish();
666 }
667 catch (Exception ex)
668 {
669 ex.printStackTrace();
670 return -1;
671 }
672 if (fd == null)
673 {
674 return -1;
675 }
676 return fd.detachFd();
677 }
678 }
679
680 /**
681 * Cache non DNS related information so we can recreate the builder without
682 * that information when reestablishing IKE_SAs
683 */
684 public class BuilderCache
685 {
686 private final List<PrefixedAddress> mAddresses = new ArrayList<PrefixedAddress>();
687 private final List<PrefixedAddress> mRoutes = new ArrayList<PrefixedAddress>();
688 private int mMtu;
689
690 public void addAddress(String address, int prefixLength)
691 {
692 mAddresses.add(new PrefixedAddress(address, prefixLength));
693 }
694
695 public void addRoute(String address, int prefixLength)
696 {
697 mRoutes.add(new PrefixedAddress(address, prefixLength));
698 }
699
700 public void setMtu(int mtu)
701 {
702 mMtu = mtu;
703 }
704
705 public void applyData(VpnService.Builder builder)
706 {
707 for (PrefixedAddress address : mAddresses)
708 {
709 builder.addAddress(address.mAddress, address.mPrefix);
710 }
711 for (PrefixedAddress route : mRoutes)
712 {
713 builder.addRoute(route.mAddress, route.mPrefix);
714 }
715 builder.setMtu(mMtu);
716 }
717
718 private class PrefixedAddress
719 {
720 public String mAddress;
721 public int mPrefix;
722
723 public PrefixedAddress(String address, int prefix)
724 {
725 this.mAddress = address;
726 this.mPrefix = prefix;
727 }
728 }
729 }
730
731 /*
732 * The libraries are extracted to /data/data/org.strongswan.android/...
733 * during installation.
734 */
735 static
736 {
737 System.loadLibrary("strongswan");
738
739 if (MainActivity.USE_BYOD)
740 {
741 System.loadLibrary("tncif");
742 System.loadLibrary("tnccs");
743 System.loadLibrary("imcv");
744 }
745
746 System.loadLibrary("hydra");
747 System.loadLibrary("charon");
748 System.loadLibrary("ipsec");
749 System.loadLibrary("androidbridge");
750 }
751 }