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