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