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