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