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