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