android: Apply configured server port
[strongswan.git] / src / frontends / android / src / org / strongswan / android / logic / CharonVpnService.java
1 /*
2 * Copyright (C) 2012-2015 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.security.PrivateKey;
22 import java.security.cert.CertificateEncodingException;
23 import java.security.cert.X509Certificate;
24 import java.util.ArrayList;
25 import java.util.List;
26 import java.util.Locale;
27
28 import org.strongswan.android.data.VpnProfile;
29 import org.strongswan.android.data.VpnProfileDataSource;
30 import org.strongswan.android.data.VpnType.VpnTypeFeature;
31 import org.strongswan.android.logic.VpnStateService.ErrorState;
32 import org.strongswan.android.logic.VpnStateService.State;
33 import org.strongswan.android.logic.imc.ImcState;
34 import org.strongswan.android.logic.imc.RemediationInstruction;
35 import org.strongswan.android.ui.MainActivity;
36 import org.strongswan.android.utils.SettingsWriter;
37
38 import android.app.PendingIntent;
39 import android.app.Service;
40 import android.content.ComponentName;
41 import android.content.Context;
42 import android.content.Intent;
43 import android.content.ServiceConnection;
44 import android.net.VpnService;
45 import android.os.Bundle;
46 import android.os.IBinder;
47 import android.os.ParcelFileDescriptor;
48 import android.security.KeyChain;
49 import android.security.KeyChainException;
50 import android.util.Log;
51
52 public class CharonVpnService extends VpnService implements Runnable
53 {
54 private static final String TAG = CharonVpnService.class.getSimpleName();
55 public static final String LOG_FILE = "charon.log";
56
57 private String mLogFile;
58 private VpnProfileDataSource mDataSource;
59 private Thread mConnectionHandler;
60 private VpnProfile mCurrentProfile;
61 private volatile String mCurrentCertificateAlias;
62 private volatile String mCurrentUserCertificateAlias;
63 private VpnProfile mNextProfile;
64 private volatile boolean mProfileUpdated;
65 private volatile boolean mTerminate;
66 private volatile boolean mIsDisconnecting;
67 private VpnStateService mService;
68 private final Object mServiceLock = new Object();
69 private final ServiceConnection mServiceConnection = new ServiceConnection() {
70 @Override
71 public void onServiceDisconnected(ComponentName name)
72 { /* since the service is local this is theoretically only called when the process is terminated */
73 synchronized (mServiceLock)
74 {
75 mService = null;
76 }
77 }
78
79 @Override
80 public void onServiceConnected(ComponentName name, IBinder service)
81 {
82 synchronized (mServiceLock)
83 {
84 mService = ((VpnStateService.LocalBinder)service).getService();
85 }
86 /* we are now ready to start the handler thread */
87 mConnectionHandler.start();
88 }
89 };
90
91 /**
92 * as defined in charonservice.h
93 */
94 static final int STATE_CHILD_SA_UP = 1;
95 static final int STATE_CHILD_SA_DOWN = 2;
96 static final int STATE_AUTH_ERROR = 3;
97 static final int STATE_PEER_AUTH_ERROR = 4;
98 static final int STATE_LOOKUP_ERROR = 5;
99 static final int STATE_UNREACHABLE_ERROR = 6;
100 static final int STATE_GENERIC_ERROR = 7;
101
102 @Override
103 public int onStartCommand(Intent intent, int flags, int startId)
104 {
105 if (intent != null)
106 {
107 Bundle bundle = intent.getExtras();
108 VpnProfile profile = null;
109 if (bundle != null)
110 {
111 profile = mDataSource.getVpnProfile(bundle.getLong(VpnProfileDataSource.KEY_ID));
112 if (profile != null)
113 {
114 String password = bundle.getString(VpnProfileDataSource.KEY_PASSWORD);
115 profile.setPassword(password);
116 }
117 }
118 setNextProfile(profile);
119 }
120 return START_NOT_STICKY;
121 }
122
123 @Override
124 public void onCreate()
125 {
126 mLogFile = getFilesDir().getAbsolutePath() + File.separator + LOG_FILE;
127
128 mDataSource = new VpnProfileDataSource(this);
129 mDataSource.open();
130 /* use a separate thread as main thread for charon */
131 mConnectionHandler = new Thread(this);
132 /* the thread is started when the service is bound */
133 bindService(new Intent(this, VpnStateService.class),
134 mServiceConnection, Service.BIND_AUTO_CREATE);
135 }
136
137 @Override
138 public void onRevoke()
139 { /* the system revoked the rights grated with the initial prepare() call.
140 * called when the user clicks disconnect in the system's VPN dialog */
141 setNextProfile(null);
142 }
143
144 @Override
145 public void onDestroy()
146 {
147 mTerminate = true;
148 setNextProfile(null);
149 try
150 {
151 mConnectionHandler.join();
152 }
153 catch (InterruptedException e)
154 {
155 e.printStackTrace();
156 }
157 if (mService != null)
158 {
159 unbindService(mServiceConnection);
160 }
161 mDataSource.close();
162 }
163
164 /**
165 * Set the profile that is to be initiated next. Notify the handler thread.
166 *
167 * @param profile the profile to initiate
168 */
169 private void setNextProfile(VpnProfile profile)
170 {
171 synchronized (this)
172 {
173 this.mNextProfile = profile;
174 mProfileUpdated = true;
175 notifyAll();
176 }
177 }
178
179 @Override
180 public void run()
181 {
182 while (true)
183 {
184 synchronized (this)
185 {
186 try
187 {
188 while (!mProfileUpdated)
189 {
190 wait();
191 }
192
193 mProfileUpdated = false;
194 stopCurrentConnection();
195 if (mNextProfile == null)
196 {
197 setState(State.DISABLED);
198 if (mTerminate)
199 {
200 break;
201 }
202 }
203 else
204 {
205 mCurrentProfile = mNextProfile;
206 mNextProfile = null;
207
208 /* store this in a separate (volatile) variable to avoid
209 * a possible deadlock during deinitialization */
210 mCurrentCertificateAlias = mCurrentProfile.getCertificateAlias();
211 mCurrentUserCertificateAlias = mCurrentProfile.getUserCertificateAlias();
212
213 startConnection(mCurrentProfile);
214 mIsDisconnecting = false;
215
216 BuilderAdapter builder = new BuilderAdapter(mCurrentProfile.getName());
217 if (initializeCharon(builder, mLogFile, mCurrentProfile.getVpnType().has(VpnTypeFeature.BYOD)))
218 {
219 Log.i(TAG, "charon started");
220 SettingsWriter writer = new SettingsWriter();
221 writer.setValue("global.language", Locale.getDefault().getLanguage());
222 writer.setValue("global.mtu", mCurrentProfile.getMTU());
223 writer.setValue("connection.type", mCurrentProfile.getVpnType().getIdentifier());
224 writer.setValue("connection.server", mCurrentProfile.getGateway());
225 writer.setValue("connection.port", mCurrentProfile.getPort());
226 writer.setValue("connection.username", mCurrentProfile.getUsername());
227 writer.setValue("connection.password", mCurrentProfile.getPassword());
228 initiate(writer.serialize());
229 }
230 else
231 {
232 Log.e(TAG, "failed to start charon");
233 setError(ErrorState.GENERIC_ERROR);
234 setState(State.DISABLED);
235 mCurrentProfile = null;
236 }
237 }
238 }
239 catch (InterruptedException ex)
240 {
241 stopCurrentConnection();
242 setState(State.DISABLED);
243 }
244 }
245 }
246 }
247
248 /**
249 * Stop any existing connection by deinitializing charon.
250 */
251 private void stopCurrentConnection()
252 {
253 synchronized (this)
254 {
255 if (mCurrentProfile != null)
256 {
257 setState(State.DISCONNECTING);
258 mIsDisconnecting = true;
259 deinitializeCharon();
260 Log.i(TAG, "charon stopped");
261 mCurrentProfile = null;
262 }
263 }
264 }
265
266 /**
267 * Notify the state service about a new connection attempt.
268 * Called by the handler thread.
269 *
270 * @param profile currently active VPN profile
271 */
272 private void startConnection(VpnProfile profile)
273 {
274 synchronized (mServiceLock)
275 {
276 if (mService != null)
277 {
278 mService.startConnection(profile);
279 }
280 }
281 }
282
283 /**
284 * Update the current VPN state on the state service. Called by the handler
285 * thread and any of charon's threads.
286 *
287 * @param state current state
288 */
289 private void setState(State state)
290 {
291 synchronized (mServiceLock)
292 {
293 if (mService != null)
294 {
295 mService.setState(state);
296 }
297 }
298 }
299
300 /**
301 * Set an error on the state service. Called by the handler thread and any
302 * of charon's threads.
303 *
304 * @param error error state
305 */
306 private void setError(ErrorState error)
307 {
308 synchronized (mServiceLock)
309 {
310 if (mService != null)
311 {
312 mService.setError(error);
313 }
314 }
315 }
316
317 /**
318 * Set the IMC state on the state service. Called by the handler thread and
319 * any of charon's threads.
320 *
321 * @param state IMC state
322 */
323 private void setImcState(ImcState state)
324 {
325 synchronized (mServiceLock)
326 {
327 if (mService != null)
328 {
329 mService.setImcState(state);
330 }
331 }
332 }
333
334 /**
335 * Set an error on the state service. Called by the handler thread and any
336 * of charon's threads.
337 *
338 * @param error error state
339 */
340 private void setErrorDisconnect(ErrorState error)
341 {
342 synchronized (mServiceLock)
343 {
344 if (mService != null)
345 {
346 if (!mIsDisconnecting)
347 {
348 mService.setError(error);
349 }
350 }
351 }
352 }
353
354 /**
355 * Updates the state of the current connection.
356 * Called via JNI by different threads (but not concurrently).
357 *
358 * @param status new state
359 */
360 public void updateStatus(int status)
361 {
362 switch (status)
363 {
364 case STATE_CHILD_SA_DOWN:
365 if (!mIsDisconnecting)
366 {
367 setState(State.CONNECTING);
368 }
369 break;
370 case STATE_CHILD_SA_UP:
371 setState(State.CONNECTED);
372 break;
373 case STATE_AUTH_ERROR:
374 setErrorDisconnect(ErrorState.AUTH_FAILED);
375 break;
376 case STATE_PEER_AUTH_ERROR:
377 setErrorDisconnect(ErrorState.PEER_AUTH_FAILED);
378 break;
379 case STATE_LOOKUP_ERROR:
380 setErrorDisconnect(ErrorState.LOOKUP_FAILED);
381 break;
382 case STATE_UNREACHABLE_ERROR:
383 setErrorDisconnect(ErrorState.UNREACHABLE);
384 break;
385 case STATE_GENERIC_ERROR:
386 setErrorDisconnect(ErrorState.GENERIC_ERROR);
387 break;
388 default:
389 Log.e(TAG, "Unknown status code received");
390 break;
391 }
392 }
393
394 /**
395 * Updates the IMC state of the current connection.
396 * Called via JNI by different threads (but not concurrently).
397 *
398 * @param value new state
399 */
400 public void updateImcState(int value)
401 {
402 ImcState state = ImcState.fromValue(value);
403 if (state != null)
404 {
405 setImcState(state);
406 }
407 }
408
409 /**
410 * Add a remediation instruction to the VPN state service.
411 * Called via JNI by different threads (but not concurrently).
412 *
413 * @param xml XML text
414 */
415 public void addRemediationInstruction(String xml)
416 {
417 for (RemediationInstruction instruction : RemediationInstruction.fromXml(xml))
418 {
419 synchronized (mServiceLock)
420 {
421 if (mService != null)
422 {
423 mService.addRemediationInstruction(instruction);
424 }
425 }
426 }
427 }
428
429 /**
430 * Function called via JNI to generate a list of DER encoded CA certificates
431 * as byte array.
432 *
433 * @return a list of DER encoded CA certificates
434 */
435 private byte[][] getTrustedCertificates()
436 {
437 ArrayList<byte[]> certs = new ArrayList<byte[]>();
438 TrustedCertificateManager certman = TrustedCertificateManager.getInstance();
439 try
440 {
441 String alias = this.mCurrentCertificateAlias;
442 if (alias != null)
443 {
444 X509Certificate cert = certman.getCACertificateFromAlias(alias);
445 if (cert == null)
446 {
447 return null;
448 }
449 certs.add(cert.getEncoded());
450 }
451 else
452 {
453 for (X509Certificate cert : certman.getAllCACertificates().values())
454 {
455 certs.add(cert.getEncoded());
456 }
457 }
458 }
459 catch (CertificateEncodingException e)
460 {
461 e.printStackTrace();
462 return null;
463 }
464 return certs.toArray(new byte[certs.size()][]);
465 }
466
467 /**
468 * Function called via JNI to get a list containing the DER encoded certificates
469 * of the user selected certificate chain (beginning with the user certificate).
470 *
471 * Since this method is called from a thread of charon's thread pool we are safe
472 * to call methods on KeyChain directly.
473 *
474 * @return list containing the certificates (first element is the user certificate)
475 * @throws InterruptedException
476 * @throws KeyChainException
477 * @throws CertificateEncodingException
478 */
479 private byte[][] getUserCertificate() throws KeyChainException, InterruptedException, CertificateEncodingException
480 {
481 ArrayList<byte[]> encodings = new ArrayList<byte[]>();
482 X509Certificate[] chain = KeyChain.getCertificateChain(getApplicationContext(), mCurrentUserCertificateAlias);
483 if (chain == null || chain.length == 0)
484 {
485 return null;
486 }
487 for (X509Certificate cert : chain)
488 {
489 encodings.add(cert.getEncoded());
490 }
491 return encodings.toArray(new byte[encodings.size()][]);
492 }
493
494 /**
495 * Function called via JNI to get the private key the user selected.
496 *
497 * Since this method is called from a thread of charon's thread pool we are safe
498 * to call methods on KeyChain directly.
499 *
500 * @return the private key
501 * @throws InterruptedException
502 * @throws KeyChainException
503 * @throws CertificateEncodingException
504 */
505 private PrivateKey getUserKey() throws KeyChainException, InterruptedException
506 {
507 return KeyChain.getPrivateKey(getApplicationContext(), mCurrentUserCertificateAlias);
508 }
509
510 /**
511 * Initialization of charon, provided by libandroidbridge.so
512 *
513 * @param builder BuilderAdapter for this connection
514 * @param logfile absolute path to the logfile
515 * @param boyd enable BYOD features
516 * @return TRUE if initialization was successful
517 */
518 public native boolean initializeCharon(BuilderAdapter builder, String logfile, boolean byod);
519
520 /**
521 * Deinitialize charon, provided by libandroidbridge.so
522 */
523 public native void deinitializeCharon();
524
525 /**
526 * Initiate VPN, provided by libandroidbridge.so
527 */
528 public native void initiate(String config);
529
530 /**
531 * Adapter for VpnService.Builder which is used to access it safely via JNI.
532 * There is a corresponding C object to access it from native code.
533 */
534 public class BuilderAdapter
535 {
536 private final String mName;
537 private VpnService.Builder mBuilder;
538 private BuilderCache mCache;
539 private BuilderCache mEstablishedCache;
540
541 public BuilderAdapter(String name)
542 {
543 mName = name;
544 mBuilder = createBuilder(name);
545 mCache = new BuilderCache();
546 }
547
548 private VpnService.Builder createBuilder(String name)
549 {
550 VpnService.Builder builder = new CharonVpnService.Builder();
551 builder.setSession(mName);
552
553 /* even though the option displayed in the system dialog says "Configure"
554 * we just use our main Activity */
555 Context context = getApplicationContext();
556 Intent intent = new Intent(context, MainActivity.class);
557 PendingIntent pending = PendingIntent.getActivity(context, 0, intent,
558 PendingIntent.FLAG_UPDATE_CURRENT);
559 builder.setConfigureIntent(pending);
560 return builder;
561 }
562
563 public synchronized boolean addAddress(String address, int prefixLength)
564 {
565 try
566 {
567 mBuilder.addAddress(address, prefixLength);
568 mCache.addAddress(address, prefixLength);
569 }
570 catch (IllegalArgumentException ex)
571 {
572 return false;
573 }
574 return true;
575 }
576
577 public synchronized boolean addDnsServer(String address)
578 {
579 try
580 {
581 mBuilder.addDnsServer(address);
582 }
583 catch (IllegalArgumentException ex)
584 {
585 return false;
586 }
587 return true;
588 }
589
590 public synchronized boolean addRoute(String address, int prefixLength)
591 {
592 try
593 {
594 mBuilder.addRoute(address, prefixLength);
595 mCache.addRoute(address, prefixLength);
596 }
597 catch (IllegalArgumentException ex)
598 {
599 return false;
600 }
601 return true;
602 }
603
604 public synchronized boolean addSearchDomain(String domain)
605 {
606 try
607 {
608 mBuilder.addSearchDomain(domain);
609 }
610 catch (IllegalArgumentException ex)
611 {
612 return false;
613 }
614 return true;
615 }
616
617 public synchronized boolean setMtu(int mtu)
618 {
619 try
620 {
621 mBuilder.setMtu(mtu);
622 mCache.setMtu(mtu);
623 }
624 catch (IllegalArgumentException ex)
625 {
626 return false;
627 }
628 return true;
629 }
630
631 public synchronized int establish()
632 {
633 ParcelFileDescriptor fd;
634 try
635 {
636 fd = mBuilder.establish();
637 }
638 catch (Exception ex)
639 {
640 ex.printStackTrace();
641 return -1;
642 }
643 if (fd == null)
644 {
645 return -1;
646 }
647 /* now that the TUN device is created we don't need the current
648 * builder anymore, but we might need another when reestablishing */
649 mBuilder = createBuilder(mName);
650 mEstablishedCache = mCache;
651 mCache = new BuilderCache();
652 return fd.detachFd();
653 }
654
655 public synchronized int establishNoDns()
656 {
657 ParcelFileDescriptor fd;
658
659 if (mEstablishedCache == null)
660 {
661 return -1;
662 }
663 try
664 {
665 Builder builder = createBuilder(mName);
666 mEstablishedCache.applyData(builder);
667 fd = builder.establish();
668 }
669 catch (Exception ex)
670 {
671 ex.printStackTrace();
672 return -1;
673 }
674 if (fd == null)
675 {
676 return -1;
677 }
678 return fd.detachFd();
679 }
680 }
681
682 /**
683 * Cache non DNS related information so we can recreate the builder without
684 * that information when reestablishing IKE_SAs
685 */
686 public class BuilderCache
687 {
688 private final List<PrefixedAddress> mAddresses = new ArrayList<PrefixedAddress>();
689 private final List<PrefixedAddress> mRoutes = new ArrayList<PrefixedAddress>();
690 private int mMtu;
691
692 public void addAddress(String address, int prefixLength)
693 {
694 mAddresses.add(new PrefixedAddress(address, prefixLength));
695 }
696
697 public void addRoute(String address, int prefixLength)
698 {
699 mRoutes.add(new PrefixedAddress(address, prefixLength));
700 }
701
702 public void setMtu(int mtu)
703 {
704 mMtu = mtu;
705 }
706
707 public void applyData(VpnService.Builder builder)
708 {
709 for (PrefixedAddress address : mAddresses)
710 {
711 builder.addAddress(address.mAddress, address.mPrefix);
712 }
713 for (PrefixedAddress route : mRoutes)
714 {
715 builder.addRoute(route.mAddress, route.mPrefix);
716 }
717 builder.setMtu(mMtu);
718 }
719
720 private class PrefixedAddress
721 {
722 public String mAddress;
723 public int mPrefix;
724
725 public PrefixedAddress(String address, int prefix)
726 {
727 this.mAddress = address;
728 this.mPrefix = prefix;
729 }
730 }
731 }
732
733 /*
734 * The libraries are extracted to /data/data/org.strongswan.android/...
735 * during installation.
736 */
737 static
738 {
739 System.loadLibrary("strongswan");
740
741 if (MainActivity.USE_BYOD)
742 {
743 System.loadLibrary("tncif");
744 System.loadLibrary("tnccs");
745 System.loadLibrary("imcv");
746 }
747
748 System.loadLibrary("hydra");
749 System.loadLibrary("charon");
750 System.loadLibrary("ipsec");
751 System.loadLibrary("androidbridge");
752 }
753 }