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