android: Fixed "Configure" button in Android VPN dialog
[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 certificates
431 * of the user selected certificate chain (beginning with the user certificate).
432 *
433 * Since this method is called from a thread of charon's thread pool we are safe
434 * to call methods on KeyChain directly.
435 *
436 * @return list containing the certificates (first element is the user certificate)
437 * @throws InterruptedException
438 * @throws KeyChainException
439 * @throws CertificateEncodingException
440 */
441 private byte[][] getUserCertificate() throws KeyChainException, InterruptedException, CertificateEncodingException
442 {
443 ArrayList<byte[]> encodings = new ArrayList<byte[]>();
444 X509Certificate[] chain = KeyChain.getCertificateChain(getApplicationContext(), mCurrentUserCertificateAlias);
445 if (chain == null || chain.length == 0)
446 {
447 return null;
448 }
449 for (X509Certificate cert : chain)
450 {
451 encodings.add(cert.getEncoded());
452 }
453 return encodings.toArray(new byte[encodings.size()][]);
454 }
455
456 /**
457 * Function called via JNI to get the private key the user selected.
458 *
459 * Since this method is called from a thread of charon's thread pool we are safe
460 * to call methods on KeyChain directly.
461 *
462 * @return the private key
463 * @throws InterruptedException
464 * @throws KeyChainException
465 * @throws CertificateEncodingException
466 */
467 private PrivateKey getUserKey() throws KeyChainException, InterruptedException
468 {
469 return KeyChain.getPrivateKey(getApplicationContext(), mCurrentUserCertificateAlias);
470
471 }
472
473 /**
474 * Initialization of charon, provided by libandroidbridge.so
475 *
476 * @param builder BuilderAdapter for this connection
477 * @param logfile absolute path to the logfile
478 */
479 public native void initializeCharon(BuilderAdapter builder, String logfile);
480
481 /**
482 * Deinitialize charon, provided by libandroidbridge.so
483 */
484 public native void deinitializeCharon();
485
486 /**
487 * Initiate VPN, provided by libandroidbridge.so
488 */
489 public native void initiate(String type, String local_address, String gateway,
490 String username, String password);
491
492 /**
493 * Helper function that retrieves a local IPv4 address.
494 *
495 * @return string representation of an IPv4 address, or null if none found
496 */
497 private static String getLocalIPv4Address()
498 {
499 try
500 {
501 Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces();
502 while (en.hasMoreElements())
503 {
504 NetworkInterface intf = en.nextElement();
505
506 Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses();
507 while (enumIpAddr.hasMoreElements())
508 {
509 InetAddress inetAddress = enumIpAddr.nextElement();
510 if (!inetAddress.isLoopbackAddress() && inetAddress.getAddress().length == 4)
511 {
512 return inetAddress.getHostAddress().toString();
513 }
514 }
515 }
516 }
517 catch (SocketException ex)
518 {
519 ex.printStackTrace();
520 return null;
521 }
522 return null;
523 }
524
525 /**
526 * Adapter for VpnService.Builder which is used to access it safely via JNI.
527 * There is a corresponding C object to access it from native code.
528 */
529 public class BuilderAdapter
530 {
531 private final String mName;
532 private VpnService.Builder mBuilder;
533
534 public BuilderAdapter(String name)
535 {
536 mName = name;
537 mBuilder = createBuilder(name);
538 }
539
540 private VpnService.Builder createBuilder(String name)
541 {
542 VpnService.Builder builder = new CharonVpnService.Builder();
543 builder.setSession(mName);
544
545 /* even though the option displayed in the system dialog says "Configure"
546 * we just use our main Activity */
547 Context context = getApplicationContext();
548 Intent intent = new Intent(context, MainActivity.class);
549 PendingIntent pending = PendingIntent.getActivity(context, 0, intent,
550 PendingIntent.FLAG_UPDATE_CURRENT);
551 builder.setConfigureIntent(pending);
552 return builder;
553 }
554
555 public synchronized boolean addAddress(String address, int prefixLength)
556 {
557 try
558 {
559 mBuilder.addAddress(address, prefixLength);
560 }
561 catch (IllegalArgumentException ex)
562 {
563 return false;
564 }
565 return true;
566 }
567
568 public synchronized boolean addDnsServer(String address)
569 {
570 try
571 {
572 mBuilder.addDnsServer(address);
573 }
574 catch (IllegalArgumentException ex)
575 {
576 return false;
577 }
578 return true;
579 }
580
581 public synchronized boolean addRoute(String address, int prefixLength)
582 {
583 try
584 {
585 mBuilder.addRoute(address, prefixLength);
586 }
587 catch (IllegalArgumentException ex)
588 {
589 return false;
590 }
591 return true;
592 }
593
594 public synchronized boolean addSearchDomain(String domain)
595 {
596 try
597 {
598 mBuilder.addSearchDomain(domain);
599 }
600 catch (IllegalArgumentException ex)
601 {
602 return false;
603 }
604 return true;
605 }
606
607 public synchronized boolean setMtu(int mtu)
608 {
609 try
610 {
611 mBuilder.setMtu(mtu);
612 }
613 catch (IllegalArgumentException ex)
614 {
615 return false;
616 }
617 return true;
618 }
619
620 public synchronized int establish()
621 {
622 ParcelFileDescriptor fd;
623 try
624 {
625 fd = mBuilder.establish();
626 }
627 catch (Exception ex)
628 {
629 ex.printStackTrace();
630 return -1;
631 }
632 if (fd == null)
633 {
634 return -1;
635 }
636 /* now that the TUN device is created we don't need the current
637 * builder anymore, but we might need another when reestablishing */
638 mBuilder = createBuilder(mName);
639 return fd.detachFd();
640 }
641 }
642
643 /*
644 * The libraries are extracted to /data/data/org.strongswan.android/...
645 * during installation.
646 */
647 static
648 {
649 System.loadLibrary("crypto");
650 System.loadLibrary("strongswan");
651 System.loadLibrary("hydra");
652 System.loadLibrary("charon");
653 System.loadLibrary("ipsec");
654 System.loadLibrary("androidbridge");
655 }
656 }