Charon logs to a file in the App's data directory
[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(local_address != null ? local_address : "0.0.0.0",
218 mCurrentProfile.getGateway(), mCurrentProfile.getUsername(),
219 mCurrentProfile.getPassword());
220 }
221 }
222 catch (InterruptedException ex)
223 {
224 stopCurrentConnection();
225 setState(State.DISABLED);
226 }
227 }
228 }
229 }
230
231 /**
232 * Stop any existing connection by deinitializing charon.
233 */
234 private void stopCurrentConnection()
235 {
236 synchronized (this)
237 {
238 if (mCurrentProfile != null)
239 {
240 setState(State.DISCONNECTING);
241 mIsDisconnecting = true;
242 deinitializeCharon();
243 Log.i(TAG, "charon stopped");
244 mCurrentProfile = null;
245 }
246 }
247 }
248
249 /**
250 * Update the VPN profile on the state service. Called by the handler thread.
251 *
252 * @param profile currently active VPN profile
253 */
254 private void setProfile(VpnProfile profile)
255 {
256 synchronized (mServiceLock)
257 {
258 if (mService != null)
259 {
260 mService.setProfile(profile);
261 }
262 }
263 }
264
265 /**
266 * Update the current VPN state on the state service. Called by the handler
267 * thread and any of charon's threads.
268 *
269 * @param state current state
270 */
271 private void setState(State state)
272 {
273 synchronized (mServiceLock)
274 {
275 if (mService != null)
276 {
277 mService.setState(state);
278 }
279 }
280 }
281
282 /**
283 * Set an error on the state service. Called by the handler thread and any
284 * of charon's threads.
285 *
286 * @param error error state
287 */
288 private void setError(ErrorState error)
289 {
290 synchronized (mServiceLock)
291 {
292 if (mService != null)
293 {
294 mService.setError(error);
295 }
296 }
297 }
298
299 /**
300 * Set an error on the state service and disconnect the current connection.
301 * This is not done by calling stopCurrentConnection() above, but instead
302 * is done asynchronously via state service.
303 *
304 * @param error error state
305 */
306 private void setErrorDisconnect(ErrorState error)
307 {
308 synchronized (mServiceLock)
309 {
310 if (mService != null)
311 {
312 mService.setError(error);
313 if (!mIsDisconnecting)
314 {
315 mService.disconnect();
316 }
317 }
318 }
319 }
320
321 /**
322 * Updates the state of the current connection.
323 * Called via JNI by different threads (but not concurrently).
324 *
325 * @param status new state
326 */
327 public void updateStatus(int status)
328 {
329 switch (status)
330 {
331 case STATE_CHILD_SA_DOWN:
332 synchronized (mServiceLock)
333 {
334 /* if we are not actively disconnecting we assume the remote terminated
335 * the connection and call disconnect() to deinitialize charon properly */
336 if (mService != null && !mIsDisconnecting)
337 {
338 mService.disconnect();
339 }
340 }
341 break;
342 case STATE_CHILD_SA_UP:
343 setState(State.CONNECTED);
344 break;
345 case STATE_AUTH_ERROR:
346 setErrorDisconnect(ErrorState.AUTH_FAILED);
347 break;
348 case STATE_PEER_AUTH_ERROR:
349 setErrorDisconnect(ErrorState.PEER_AUTH_FAILED);
350 break;
351 case STATE_LOOKUP_ERROR:
352 setErrorDisconnect(ErrorState.LOOKUP_FAILED);
353 break;
354 case STATE_UNREACHABLE_ERROR:
355 setErrorDisconnect(ErrorState.UNREACHABLE);
356 break;
357 case STATE_GENERIC_ERROR:
358 setErrorDisconnect(ErrorState.GENERIC_ERROR);
359 break;
360 default:
361 Log.e(TAG, "Unknown status code received");
362 break;
363 }
364 }
365
366 /**
367 * Function called via JNI to generate a list of DER encoded CA certificates
368 * as byte array.
369 *
370 * @param hash optional alias (only hash part), if given matching certificates are returned
371 * @return a list of DER encoded CA certificates
372 */
373 private byte[][] getTrustedCertificates(String hash)
374 {
375 ArrayList<byte[]> certs = new ArrayList<byte[]>();
376 TrustedCertificateManager certman = TrustedCertificateManager.getInstance();
377 try
378 {
379 if (hash != null)
380 {
381 String alias = "user:" + hash + ".0";
382 X509Certificate cert = certman.getCACertificateFromAlias(alias);
383 if (cert == null)
384 {
385 alias = "system:" + hash + ".0";
386 cert = certman.getCACertificateFromAlias(alias);
387 }
388 if (cert == null)
389 {
390 return null;
391 }
392 certs.add(cert.getEncoded());
393 }
394 else
395 {
396 String alias = this.mCurrentCertificateAlias;
397 if (alias != null)
398 {
399 X509Certificate cert = certman.getCACertificateFromAlias(alias);
400 if (cert == null)
401 {
402 return null;
403 }
404 certs.add(cert.getEncoded());
405 }
406 else
407 {
408 for (X509Certificate cert : certman.getAllCACertificates().values())
409 {
410 certs.add(cert.getEncoded());
411 }
412 }
413 }
414 }
415 catch (CertificateEncodingException e)
416 {
417 e.printStackTrace();
418 return null;
419 }
420 return certs.toArray(new byte[certs.size()][]);
421 }
422
423 /**
424 * Initialization of charon, provided by libandroidbridge.so
425 *
426 * @param builder BuilderAdapter for this connection
427 * @param logfile absolute path to the logfile
428 */
429 public native void initializeCharon(BuilderAdapter builder, String logfile);
430
431 /**
432 * Deinitialize charon, provided by libandroidbridge.so
433 */
434 public native void deinitializeCharon();
435
436 /**
437 * Initiate VPN, provided by libandroidbridge.so
438 */
439 public native void initiate(String local_address, String gateway,
440 String username, String password);
441
442 /**
443 * Helper function that retrieves a local IPv4 address.
444 *
445 * @return string representation of an IPv4 address, or null if none found
446 */
447 private static String getLocalIPv4Address()
448 {
449 try
450 {
451 Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces();
452 while (en.hasMoreElements())
453 {
454 NetworkInterface intf = en.nextElement();
455
456 Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses();
457 while (enumIpAddr.hasMoreElements())
458 {
459 InetAddress inetAddress = enumIpAddr.nextElement();
460 if (!inetAddress.isLoopbackAddress() && inetAddress.getAddress().length == 4)
461 {
462 return inetAddress.getHostAddress().toString();
463 }
464 }
465 }
466 }
467 catch (SocketException ex)
468 {
469 ex.printStackTrace();
470 return null;
471 }
472 return null;
473 }
474
475 /**
476 * Adapter for VpnService.Builder which is used to access it safely via JNI.
477 * There is a corresponding C object to access it from native code.
478 */
479 public class BuilderAdapter
480 {
481 VpnService.Builder builder;
482
483 public BuilderAdapter(String name)
484 {
485 builder = new CharonVpnService.Builder();
486 builder.setSession(name);
487
488 /* even though the option displayed in the system dialog says "Configure"
489 * we just use our main Activity */
490 Context context = getApplicationContext();
491 Intent intent = new Intent(context, MainActivity.class);
492 PendingIntent pending = PendingIntent.getActivity(context, 0, intent,
493 Intent.FLAG_ACTIVITY_NEW_TASK);
494 builder.setConfigureIntent(pending);
495 }
496
497 public synchronized boolean addAddress(String address, int prefixLength)
498 {
499 try
500 {
501 builder.addAddress(address, prefixLength);
502 }
503 catch (IllegalArgumentException ex)
504 {
505 return false;
506 }
507 return true;
508 }
509
510 public synchronized boolean addDnsServer(String address)
511 {
512 try
513 {
514 builder.addDnsServer(address);
515 }
516 catch (IllegalArgumentException ex)
517 {
518 return false;
519 }
520 return true;
521 }
522
523 public synchronized boolean addRoute(String address, int prefixLength)
524 {
525 try
526 {
527 builder.addRoute(address, prefixLength);
528 }
529 catch (IllegalArgumentException ex)
530 {
531 return false;
532 }
533 return true;
534 }
535
536 public synchronized boolean addSearchDomain(String domain)
537 {
538 try
539 {
540 builder.addSearchDomain(domain);
541 }
542 catch (IllegalArgumentException ex)
543 {
544 return false;
545 }
546 return true;
547 }
548
549 public synchronized boolean setMtu(int mtu)
550 {
551 try
552 {
553 builder.setMtu(mtu);
554 }
555 catch (IllegalArgumentException ex)
556 {
557 return false;
558 }
559 return true;
560 }
561
562 public synchronized int establish()
563 {
564 ParcelFileDescriptor fd;
565 try
566 {
567 fd = builder.establish();
568 }
569 catch (Exception ex)
570 {
571 ex.printStackTrace();
572 return -1;
573 }
574 if (fd == null)
575 {
576 return -1;
577 }
578 return fd.detachFd();
579 }
580 }
581
582 /*
583 * The libraries are extracted to /data/data/org.strongswan.android/...
584 * during installation.
585 */
586 static
587 {
588 System.loadLibrary("crypto");
589 System.loadLibrary("strongswan");
590 System.loadLibrary("hydra");
591 System.loadLibrary("charon");
592 System.loadLibrary("ipsec");
593 System.loadLibrary("androidbridge");
594 }
595 }