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