]> git.ipfire.org Git - thirdparty/strongswan.git/blob - src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java
android: Show an error if client certificate is unavailable
[thirdparty/strongswan.git] / src / frontends / android / app / src / main / java / org / strongswan / android / logic / CharonVpnService.java
1 /*
2 * Copyright (C) 2012-2018 Tobias Brunner
3 * Copyright (C) 2012 Giuliano Grassi
4 * Copyright (C) 2012 Ralf Sager
5 * HSR 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 android.annotation.TargetApi;
21 import android.app.Notification;
22 import android.app.NotificationChannel;
23 import android.app.NotificationManager;
24 import android.app.PendingIntent;
25 import android.app.Service;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.ServiceConnection;
30 import android.content.SharedPreferences;
31 import android.content.pm.PackageManager;
32 import android.net.VpnService;
33 import android.os.Build;
34 import android.os.Bundle;
35 import android.os.Handler;
36 import android.os.IBinder;
37 import android.os.ParcelFileDescriptor;
38 import android.preference.PreferenceManager;
39 import android.security.KeyChain;
40 import android.security.KeyChainException;
41 import android.support.v4.app.NotificationCompat;
42 import android.support.v4.content.ContextCompat;
43 import android.system.OsConstants;
44 import android.util.Log;
45
46 import org.strongswan.android.R;
47 import org.strongswan.android.data.VpnProfile;
48 import org.strongswan.android.data.VpnProfile.SelectedAppsHandling;
49 import org.strongswan.android.data.VpnProfileDataSource;
50 import org.strongswan.android.data.VpnType.VpnTypeFeature;
51 import org.strongswan.android.logic.VpnStateService.ErrorState;
52 import org.strongswan.android.logic.VpnStateService.State;
53 import org.strongswan.android.logic.imc.ImcState;
54 import org.strongswan.android.logic.imc.RemediationInstruction;
55 import org.strongswan.android.ui.MainActivity;
56 import org.strongswan.android.ui.VpnProfileControlActivity;
57 import org.strongswan.android.utils.Constants;
58 import org.strongswan.android.utils.IPRange;
59 import org.strongswan.android.utils.IPRangeSet;
60 import org.strongswan.android.utils.SettingsWriter;
61
62 import java.io.File;
63 import java.net.Inet4Address;
64 import java.net.Inet6Address;
65 import java.net.InetAddress;
66 import java.net.UnknownHostException;
67 import java.security.PrivateKey;
68 import java.security.cert.CertificateEncodingException;
69 import java.security.cert.X509Certificate;
70 import java.util.ArrayList;
71 import java.util.List;
72 import java.util.Locale;
73 import java.util.SortedSet;
74
75 public class CharonVpnService extends VpnService implements Runnable, VpnStateService.VpnStateListener
76 {
77 private static final String TAG = CharonVpnService.class.getSimpleName();
78 private static final String VPN_SERVICE_ACTION = "android.net.VpnService";
79 public static final String DISCONNECT_ACTION = "org.strongswan.android.CharonVpnService.DISCONNECT";
80 private static final String NOTIFICATION_CHANNEL = "org.strongswan.android.CharonVpnService.VPN_STATE_NOTIFICATION";
81 public static final String LOG_FILE = "charon.log";
82 public static final int VPN_STATE_NOTIFICATION_ID = 1;
83
84 private String mLogFile;
85 private String mAppDir;
86 private VpnProfileDataSource mDataSource;
87 private Thread mConnectionHandler;
88 private VpnProfile mCurrentProfile;
89 private volatile String mCurrentCertificateAlias;
90 private volatile String mCurrentUserCertificateAlias;
91 private VpnProfile mNextProfile;
92 private volatile boolean mProfileUpdated;
93 private volatile boolean mTerminate;
94 private volatile boolean mIsDisconnecting;
95 private volatile boolean mShowNotification;
96 private Handler mHandler;
97 private VpnStateService mService;
98 private final Object mServiceLock = new Object();
99 private final ServiceConnection mServiceConnection = new ServiceConnection() {
100 @Override
101 public void onServiceDisconnected(ComponentName name)
102 { /* since the service is local this is theoretically only called when the process is terminated */
103 synchronized (mServiceLock)
104 {
105 mService = null;
106 }
107 }
108
109 @Override
110 public void onServiceConnected(ComponentName name, IBinder service)
111 {
112 synchronized (mServiceLock)
113 {
114 mService = ((VpnStateService.LocalBinder)service).getService();
115 }
116 /* we are now ready to start the handler thread */
117 mService.registerListener(CharonVpnService.this);
118 mConnectionHandler.start();
119 }
120 };
121
122 /**
123 * as defined in charonservice.h
124 */
125 static final int STATE_CHILD_SA_UP = 1;
126 static final int STATE_CHILD_SA_DOWN = 2;
127 static final int STATE_AUTH_ERROR = 3;
128 static final int STATE_PEER_AUTH_ERROR = 4;
129 static final int STATE_LOOKUP_ERROR = 5;
130 static final int STATE_UNREACHABLE_ERROR = 6;
131 static final int STATE_CERTIFICATE_UNAVAILABLE = 7;
132 static final int STATE_GENERIC_ERROR = 8;
133
134 @Override
135 public int onStartCommand(Intent intent, int flags, int startId)
136 {
137 if (intent != null)
138 {
139 VpnProfile profile = null;
140
141 if (VPN_SERVICE_ACTION.equals(intent.getAction()))
142 { /* triggered when Always-on VPN is activated */
143 SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
144 String uuid = pref.getString(Constants.PREF_DEFAULT_VPN_PROFILE, null);
145 if (uuid == null || uuid.equals(Constants.PREF_DEFAULT_VPN_PROFILE_MRU))
146 {
147 uuid = pref.getString(Constants.PREF_MRU_VPN_PROFILE, null);
148 }
149 profile = mDataSource.getVpnProfile(uuid);
150 }
151 else if (!DISCONNECT_ACTION.equals(intent.getAction()))
152 {
153 Bundle bundle = intent.getExtras();
154 if (bundle != null)
155 {
156 profile = mDataSource.getVpnProfile(bundle.getLong(VpnProfileDataSource.KEY_ID));
157 if (profile != null)
158 {
159 String password = bundle.getString(VpnProfileDataSource.KEY_PASSWORD);
160 profile.setPassword(password);
161
162 SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
163 pref.edit().putString(Constants.PREF_MRU_VPN_PROFILE, profile.getUUID().toString())
164 .apply();
165 }
166 }
167 }
168 setNextProfile(profile);
169 }
170 return START_NOT_STICKY;
171 }
172
173 @Override
174 public void onCreate()
175 {
176 mLogFile = getFilesDir().getAbsolutePath() + File.separator + LOG_FILE;
177 mAppDir = getFilesDir().getAbsolutePath();
178
179 /* handler used to do changes in the main UI thread */
180 mHandler = new Handler();
181
182 mDataSource = new VpnProfileDataSource(this);
183 mDataSource.open();
184 /* use a separate thread as main thread for charon */
185 mConnectionHandler = new Thread(this);
186 /* the thread is started when the service is bound */
187 bindService(new Intent(this, VpnStateService.class),
188 mServiceConnection, Service.BIND_AUTO_CREATE);
189
190 createNotificationChannel();
191 }
192
193 @Override
194 public void onRevoke()
195 { /* the system revoked the rights grated with the initial prepare() call.
196 * called when the user clicks disconnect in the system's VPN dialog */
197 setNextProfile(null);
198 }
199
200 @Override
201 public void onDestroy()
202 {
203 mTerminate = true;
204 setNextProfile(null);
205 try
206 {
207 mConnectionHandler.join();
208 }
209 catch (InterruptedException e)
210 {
211 e.printStackTrace();
212 }
213 if (mService != null)
214 {
215 mService.unregisterListener(this);
216 unbindService(mServiceConnection);
217 }
218 mDataSource.close();
219 }
220
221 /**
222 * Set the profile that is to be initiated next. Notify the handler thread.
223 *
224 * @param profile the profile to initiate
225 */
226 private void setNextProfile(VpnProfile profile)
227 {
228 synchronized (this)
229 {
230 this.mNextProfile = profile;
231 mProfileUpdated = true;
232 notifyAll();
233 }
234 }
235
236 @Override
237 public void run()
238 {
239 while (true)
240 {
241 synchronized (this)
242 {
243 try
244 {
245 while (!mProfileUpdated)
246 {
247 wait();
248 }
249
250 mProfileUpdated = false;
251 stopCurrentConnection();
252 if (mNextProfile == null)
253 {
254 setState(State.DISABLED);
255 if (mTerminate)
256 {
257 break;
258 }
259 }
260 else
261 {
262 mCurrentProfile = mNextProfile;
263 mNextProfile = null;
264
265 /* store this in a separate (volatile) variable to avoid
266 * a possible deadlock during deinitialization */
267 mCurrentCertificateAlias = mCurrentProfile.getCertificateAlias();
268 mCurrentUserCertificateAlias = mCurrentProfile.getUserCertificateAlias();
269
270 startConnection(mCurrentProfile);
271 mIsDisconnecting = false;
272
273 addNotification();
274 BuilderAdapter builder = new BuilderAdapter(mCurrentProfile);
275 if (initializeCharon(builder, mLogFile, mAppDir, mCurrentProfile.getVpnType().has(VpnTypeFeature.BYOD)))
276 {
277 Log.i(TAG, "charon started");
278
279 if (mCurrentProfile.getVpnType().has(VpnTypeFeature.USER_PASS) &&
280 mCurrentProfile.getPassword() == null)
281 { /* this can happen if Always-on VPN is enabled with an incomplete profile */
282 setError(ErrorState.PASSWORD_MISSING);
283 continue;
284 }
285
286 SettingsWriter writer = new SettingsWriter();
287 writer.setValue("global.language", Locale.getDefault().getLanguage());
288 writer.setValue("global.mtu", mCurrentProfile.getMTU());
289 writer.setValue("global.nat_keepalive", mCurrentProfile.getNATKeepAlive());
290 writer.setValue("connection.type", mCurrentProfile.getVpnType().getIdentifier());
291 writer.setValue("connection.server", mCurrentProfile.getGateway());
292 writer.setValue("connection.port", mCurrentProfile.getPort());
293 writer.setValue("connection.username", mCurrentProfile.getUsername());
294 writer.setValue("connection.password", mCurrentProfile.getPassword());
295 writer.setValue("connection.local_id", mCurrentProfile.getLocalId());
296 writer.setValue("connection.remote_id", mCurrentProfile.getRemoteId());
297 writer.setValue("connection.certreq", (mCurrentProfile.getFlags() & VpnProfile.FLAGS_SUPPRESS_CERT_REQS) == 0);
298 writer.setValue("connection.ike_proposal", mCurrentProfile.getIkeProposal());
299 writer.setValue("connection.esp_proposal", mCurrentProfile.getEspProposal());
300 initiate(writer.serialize());
301 }
302 else
303 {
304 Log.e(TAG, "failed to start charon");
305 setError(ErrorState.GENERIC_ERROR);
306 setState(State.DISABLED);
307 mCurrentProfile = null;
308 }
309 }
310 }
311 catch (InterruptedException ex)
312 {
313 stopCurrentConnection();
314 setState(State.DISABLED);
315 }
316 }
317 }
318 }
319
320 /**
321 * Stop any existing connection by deinitializing charon.
322 */
323 private void stopCurrentConnection()
324 {
325 synchronized (this)
326 {
327 if (mCurrentProfile != null)
328 {
329 setState(State.DISCONNECTING);
330 mIsDisconnecting = true;
331 deinitializeCharon();
332 Log.i(TAG, "charon stopped");
333 mCurrentProfile = null;
334 removeNotification();
335 }
336 }
337 }
338
339 /**
340 * Add a permanent notification while we are connected to avoid the service getting killed by
341 * the system when low on memory.
342 */
343 private void addNotification()
344 {
345 mHandler.post(new Runnable()
346 {
347 @Override
348 public void run()
349 {
350 mShowNotification = true;
351 startForeground(VPN_STATE_NOTIFICATION_ID, buildNotification(false));
352 }
353 });
354 }
355
356 /**
357 * Remove the permanent notification.
358 */
359 private void removeNotification()
360 {
361 mHandler.post(new Runnable()
362 {
363 @Override
364 public void run()
365 {
366 mShowNotification = false;
367 stopForeground(true);
368 }
369 });
370 }
371
372 /**
373 * Create a notification channel for Android 8+
374 */
375 private void createNotificationChannel()
376 {
377 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
378 {
379 NotificationChannel channel;
380 channel = new NotificationChannel(NOTIFICATION_CHANNEL, getString(R.string.permanent_notification_name),
381 NotificationManager.IMPORTANCE_LOW);
382 channel.setDescription(getString(R.string.permanent_notification_description));
383 channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
384 channel.setShowBadge(false);
385 NotificationManager notificationManager = getSystemService(NotificationManager.class);
386 notificationManager.createNotificationChannel(channel);
387 }
388 }
389
390
391 /**
392 * Build a notification matching the current state
393 */
394 private Notification buildNotification(boolean publicVersion)
395 {
396 VpnProfile profile = mService.getProfile();
397 State state = mService.getState();
398 ErrorState error = mService.getErrorState();
399 String name = "";
400 boolean add_action = false;
401
402 if (profile != null)
403 {
404 name = profile.getName();
405 }
406 NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL)
407 .setSmallIcon(R.drawable.ic_notification)
408 .setCategory(NotificationCompat.CATEGORY_SERVICE)
409 .setVisibility(publicVersion ? NotificationCompat.VISIBILITY_PUBLIC
410 : NotificationCompat.VISIBILITY_PRIVATE);
411 int s = R.string.state_disabled;
412 if (error != ErrorState.NO_ERROR)
413 {
414 s = mService.getErrorText();
415 builder.setSmallIcon(R.drawable.ic_notification_warning);
416 builder.setColor(ContextCompat.getColor(this, R.color.error_text));
417 builder.setContentText(getString(R.string.tap_for_details));
418 }
419 else
420 {
421 switch (state)
422 {
423 case CONNECTING:
424 s = R.string.state_connecting;
425 builder.setSmallIcon(R.drawable.ic_notification_connecting);
426 builder.setColor(ContextCompat.getColor(this, R.color.warning_text));
427 add_action = true;
428 break;
429 case CONNECTED:
430 s = R.string.state_connected;
431 builder.setColor(ContextCompat.getColor(this, R.color.success_text));
432 builder.setUsesChronometer(true);
433 add_action = true;
434 break;
435 case DISCONNECTING:
436 s = R.string.state_disconnecting;
437 break;
438 }
439 }
440 builder.setContentTitle(getString(s));
441 if (!publicVersion)
442 {
443 if (add_action)
444 {
445 Intent intent = new Intent(getApplicationContext(), VpnProfileControlActivity.class);
446 intent.setAction(VpnProfileControlActivity.DISCONNECT);
447 PendingIntent pending = PendingIntent.getActivity(getApplicationContext(), 0, intent,
448 PendingIntent.FLAG_UPDATE_CURRENT);
449 builder.addAction(R.drawable.ic_notification_disconnect, getString(R.string.disconnect), pending);
450 }
451 if (error == ErrorState.NO_ERROR)
452 {
453 builder.setContentText(name);
454 }
455 builder.setPublicVersion(buildNotification(true));
456 }
457
458 Intent intent = new Intent(getApplicationContext(), MainActivity.class);
459 PendingIntent pending = PendingIntent.getActivity(getApplicationContext(), 0, intent,
460 PendingIntent.FLAG_UPDATE_CURRENT);
461 builder.setContentIntent(pending);
462 return builder.build();
463 }
464
465 @Override
466 public void stateChanged() {
467 if (mShowNotification)
468 {
469 NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
470 manager.notify(VPN_STATE_NOTIFICATION_ID, buildNotification(false));
471 }
472 }
473
474 /**
475 * Notify the state service about a new connection attempt.
476 * Called by the handler thread.
477 *
478 * @param profile currently active VPN profile
479 */
480 private void startConnection(VpnProfile profile)
481 {
482 synchronized (mServiceLock)
483 {
484 if (mService != null)
485 {
486 mService.startConnection(profile);
487 }
488 }
489 }
490
491 /**
492 * Update the current VPN state on the state service. Called by the handler
493 * thread and any of charon's threads.
494 *
495 * @param state current state
496 */
497 private void setState(State state)
498 {
499 synchronized (mServiceLock)
500 {
501 if (mService != null)
502 {
503 mService.setState(state);
504 }
505 }
506 }
507
508 /**
509 * Set an error on the state service. Called by the handler thread and any
510 * of charon's threads.
511 *
512 * @param error error state
513 */
514 private void setError(ErrorState error)
515 {
516 synchronized (mServiceLock)
517 {
518 if (mService != null)
519 {
520 mService.setError(error);
521 }
522 }
523 }
524
525 /**
526 * Set the IMC state on the state service. Called by the handler thread and
527 * any of charon's threads.
528 *
529 * @param state IMC state
530 */
531 private void setImcState(ImcState state)
532 {
533 synchronized (mServiceLock)
534 {
535 if (mService != null)
536 {
537 mService.setImcState(state);
538 }
539 }
540 }
541
542 /**
543 * Set an error on the state service. Called by the handler thread and any
544 * of charon's threads.
545 *
546 * @param error error state
547 */
548 private void setErrorDisconnect(ErrorState error)
549 {
550 synchronized (mServiceLock)
551 {
552 if (mService != null)
553 {
554 if (!mIsDisconnecting)
555 {
556 mService.setError(error);
557 }
558 }
559 }
560 }
561
562 /**
563 * Updates the state of the current connection.
564 * Called via JNI by different threads (but not concurrently).
565 *
566 * @param status new state
567 */
568 public void updateStatus(int status)
569 {
570 switch (status)
571 {
572 case STATE_CHILD_SA_DOWN:
573 if (!mIsDisconnecting)
574 {
575 setState(State.CONNECTING);
576 }
577 break;
578 case STATE_CHILD_SA_UP:
579 setState(State.CONNECTED);
580 break;
581 case STATE_AUTH_ERROR:
582 setErrorDisconnect(ErrorState.AUTH_FAILED);
583 break;
584 case STATE_PEER_AUTH_ERROR:
585 setErrorDisconnect(ErrorState.PEER_AUTH_FAILED);
586 break;
587 case STATE_LOOKUP_ERROR:
588 setErrorDisconnect(ErrorState.LOOKUP_FAILED);
589 break;
590 case STATE_UNREACHABLE_ERROR:
591 setErrorDisconnect(ErrorState.UNREACHABLE);
592 break;
593 case STATE_CERTIFICATE_UNAVAILABLE:
594 setErrorDisconnect(ErrorState.CERTIFICATE_UNAVAILABLE);
595 break;
596 case STATE_GENERIC_ERROR:
597 setErrorDisconnect(ErrorState.GENERIC_ERROR);
598 break;
599 default:
600 Log.e(TAG, "Unknown status code received");
601 break;
602 }
603 }
604
605 /**
606 * Updates the IMC state of the current connection.
607 * Called via JNI by different threads (but not concurrently).
608 *
609 * @param value new state
610 */
611 public void updateImcState(int value)
612 {
613 ImcState state = ImcState.fromValue(value);
614 if (state != null)
615 {
616 setImcState(state);
617 }
618 }
619
620 /**
621 * Add a remediation instruction to the VPN state service.
622 * Called via JNI by different threads (but not concurrently).
623 *
624 * @param xml XML text
625 */
626 public void addRemediationInstruction(String xml)
627 {
628 for (RemediationInstruction instruction : RemediationInstruction.fromXml(xml))
629 {
630 synchronized (mServiceLock)
631 {
632 if (mService != null)
633 {
634 mService.addRemediationInstruction(instruction);
635 }
636 }
637 }
638 }
639
640 /**
641 * Function called via JNI to generate a list of DER encoded CA certificates
642 * as byte array.
643 *
644 * @return a list of DER encoded CA certificates
645 */
646 private byte[][] getTrustedCertificates()
647 {
648 ArrayList<byte[]> certs = new ArrayList<byte[]>();
649 TrustedCertificateManager certman = TrustedCertificateManager.getInstance().load();
650 try
651 {
652 String alias = this.mCurrentCertificateAlias;
653 if (alias != null)
654 {
655 X509Certificate cert = certman.getCACertificateFromAlias(alias);
656 if (cert == null)
657 {
658 return null;
659 }
660 certs.add(cert.getEncoded());
661 }
662 else
663 {
664 for (X509Certificate cert : certman.getAllCACertificates().values())
665 {
666 certs.add(cert.getEncoded());
667 }
668 }
669 }
670 catch (CertificateEncodingException e)
671 {
672 e.printStackTrace();
673 return null;
674 }
675 return certs.toArray(new byte[certs.size()][]);
676 }
677
678 /**
679 * Function called via JNI to get a list containing the DER encoded certificates
680 * of the user selected certificate chain (beginning with the user certificate).
681 *
682 * Since this method is called from a thread of charon's thread pool we are safe
683 * to call methods on KeyChain directly.
684 *
685 * @return list containing the certificates (first element is the user certificate)
686 * @throws InterruptedException
687 * @throws KeyChainException
688 * @throws CertificateEncodingException
689 */
690 private byte[][] getUserCertificate() throws KeyChainException, InterruptedException, CertificateEncodingException
691 {
692 ArrayList<byte[]> encodings = new ArrayList<byte[]>();
693 X509Certificate[] chain = KeyChain.getCertificateChain(getApplicationContext(), mCurrentUserCertificateAlias);
694 if (chain == null || chain.length == 0)
695 {
696 return null;
697 }
698 for (X509Certificate cert : chain)
699 {
700 encodings.add(cert.getEncoded());
701 }
702 return encodings.toArray(new byte[encodings.size()][]);
703 }
704
705 /**
706 * Function called via JNI to get the private key the user selected.
707 *
708 * Since this method is called from a thread of charon's thread pool we are safe
709 * to call methods on KeyChain directly.
710 *
711 * @return the private key
712 * @throws InterruptedException
713 * @throws KeyChainException
714 */
715 private PrivateKey getUserKey() throws KeyChainException, InterruptedException
716 {
717 return KeyChain.getPrivateKey(getApplicationContext(), mCurrentUserCertificateAlias);
718 }
719
720 /**
721 * Initialization of charon, provided by libandroidbridge.so
722 *
723 * @param builder BuilderAdapter for this connection
724 * @param logfile absolute path to the logfile
725 * @param appdir absolute path to the data directory of the app
726 * @param byod enable BYOD features
727 * @return TRUE if initialization was successful
728 */
729 public native boolean initializeCharon(BuilderAdapter builder, String logfile, String appdir, boolean byod);
730
731 /**
732 * Deinitialize charon, provided by libandroidbridge.so
733 */
734 public native void deinitializeCharon();
735
736 /**
737 * Initiate VPN, provided by libandroidbridge.so
738 */
739 public native void initiate(String config);
740
741 /**
742 * Adapter for VpnService.Builder which is used to access it safely via JNI.
743 * There is a corresponding C object to access it from native code.
744 */
745 public class BuilderAdapter
746 {
747 private final VpnProfile mProfile;
748 private VpnService.Builder mBuilder;
749 private BuilderCache mCache;
750 private BuilderCache mEstablishedCache;
751
752 public BuilderAdapter(VpnProfile profile)
753 {
754 mProfile = profile;
755 mBuilder = createBuilder(mProfile.getName());
756 mCache = new BuilderCache(mProfile);
757 }
758
759 private VpnService.Builder createBuilder(String name)
760 {
761 VpnService.Builder builder = new CharonVpnService.Builder();
762 builder.setSession(name);
763
764 /* even though the option displayed in the system dialog says "Configure"
765 * we just use our main Activity */
766 Context context = getApplicationContext();
767 Intent intent = new Intent(context, MainActivity.class);
768 PendingIntent pending = PendingIntent.getActivity(context, 0, intent,
769 PendingIntent.FLAG_UPDATE_CURRENT);
770 builder.setConfigureIntent(pending);
771 return builder;
772 }
773
774 public synchronized boolean addAddress(String address, int prefixLength)
775 {
776 try
777 {
778 mCache.addAddress(address, prefixLength);
779 }
780 catch (IllegalArgumentException ex)
781 {
782 return false;
783 }
784 return true;
785 }
786
787 public synchronized boolean addDnsServer(String address)
788 {
789 try
790 {
791 mBuilder.addDnsServer(address);
792 mCache.recordAddressFamily(address);
793 }
794 catch (IllegalArgumentException ex)
795 {
796 return false;
797 }
798 return true;
799 }
800
801 public synchronized boolean addRoute(String address, int prefixLength)
802 {
803 try
804 {
805 mCache.addRoute(address, prefixLength);
806 }
807 catch (IllegalArgumentException ex)
808 {
809 return false;
810 }
811 return true;
812 }
813
814 public synchronized boolean addSearchDomain(String domain)
815 {
816 try
817 {
818 mBuilder.addSearchDomain(domain);
819 }
820 catch (IllegalArgumentException ex)
821 {
822 return false;
823 }
824 return true;
825 }
826
827 public synchronized boolean setMtu(int mtu)
828 {
829 try
830 {
831 mCache.setMtu(mtu);
832 }
833 catch (IllegalArgumentException ex)
834 {
835 return false;
836 }
837 return true;
838 }
839
840 public synchronized int establish()
841 {
842 ParcelFileDescriptor fd;
843 try
844 {
845 mCache.applyData(mBuilder);
846 fd = mBuilder.establish();
847 }
848 catch (Exception ex)
849 {
850 ex.printStackTrace();
851 return -1;
852 }
853 if (fd == null)
854 {
855 return -1;
856 }
857 /* now that the TUN device is created we don't need the current
858 * builder anymore, but we might need another when reestablishing */
859 mBuilder = createBuilder(mProfile.getName());
860 mEstablishedCache = mCache;
861 mCache = new BuilderCache(mProfile);
862 return fd.detachFd();
863 }
864
865 public synchronized int establishNoDns()
866 {
867 ParcelFileDescriptor fd;
868
869 if (mEstablishedCache == null)
870 {
871 return -1;
872 }
873 try
874 {
875 Builder builder = createBuilder(mProfile.getName());
876 mEstablishedCache.applyData(builder);
877 fd = builder.establish();
878 }
879 catch (Exception ex)
880 {
881 ex.printStackTrace();
882 return -1;
883 }
884 if (fd == null)
885 {
886 return -1;
887 }
888 return fd.detachFd();
889 }
890 }
891
892 /**
893 * Cache non DNS related information so we can recreate the builder without
894 * that information when reestablishing IKE_SAs
895 */
896 public class BuilderCache
897 {
898 private final List<IPRange> mAddresses = new ArrayList<>();
899 private final List<IPRange> mRoutesIPv4 = new ArrayList<>();
900 private final List<IPRange> mRoutesIPv6 = new ArrayList<>();
901 private final IPRangeSet mIncludedSubnetsv4 = new IPRangeSet();
902 private final IPRangeSet mIncludedSubnetsv6 = new IPRangeSet();
903 private final IPRangeSet mExcludedSubnets;
904 private final int mSplitTunneling;
905 private final SelectedAppsHandling mAppHandling;
906 private final SortedSet<String> mSelectedApps;
907 private int mMtu;
908 private boolean mIPv4Seen, mIPv6Seen;
909
910 public BuilderCache(VpnProfile profile)
911 {
912 IPRangeSet included = IPRangeSet.fromString(profile.getIncludedSubnets());
913 for (IPRange range : included)
914 {
915 if (range.getFrom() instanceof Inet4Address)
916 {
917 mIncludedSubnetsv4.add(range);
918 }
919 else if (range.getFrom() instanceof Inet6Address)
920 {
921 mIncludedSubnetsv6.add(range);
922 }
923 }
924 mExcludedSubnets = IPRangeSet.fromString(profile.getExcludedSubnets());
925 Integer splitTunneling = profile.getSplitTunneling();
926 mSplitTunneling = splitTunneling != null ? splitTunneling : 0;
927 mAppHandling = profile.getSelectedAppsHandling();
928 mSelectedApps = profile.getSelectedAppsSet();
929 }
930
931 public void addAddress(String address, int prefixLength)
932 {
933 try
934 {
935 mAddresses.add(new IPRange(address, prefixLength));
936 recordAddressFamily(address);
937 }
938 catch (UnknownHostException ex)
939 {
940 ex.printStackTrace();
941 }
942 }
943
944 public void addRoute(String address, int prefixLength)
945 {
946 try
947 {
948 if (isIPv6(address))
949 {
950 mRoutesIPv6.add(new IPRange(address, prefixLength));
951 }
952 else
953 {
954 mRoutesIPv4.add(new IPRange(address, prefixLength));
955 }
956 }
957 catch (UnknownHostException ex)
958 {
959 ex.printStackTrace();
960 }
961 }
962
963 public void setMtu(int mtu)
964 {
965 mMtu = mtu;
966 }
967
968 public void recordAddressFamily(String address)
969 {
970 try
971 {
972 if (isIPv6(address))
973 {
974 mIPv6Seen = true;
975 }
976 else
977 {
978 mIPv4Seen = true;
979 }
980 }
981 catch (UnknownHostException ex)
982 {
983 ex.printStackTrace();
984 }
985 }
986
987 @TargetApi(Build.VERSION_CODES.LOLLIPOP)
988 public void applyData(VpnService.Builder builder)
989 {
990 for (IPRange address : mAddresses)
991 {
992 builder.addAddress(address.getFrom(), address.getPrefix());
993 }
994 /* add routes depending on whether split tunneling is allowed or not,
995 * that is, whether we have to handle and block non-VPN traffic */
996 if ((mSplitTunneling & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) == 0)
997 {
998 if (mIPv4Seen)
999 { /* split tunneling is used depending on the routes and configuration */
1000 IPRangeSet ranges = new IPRangeSet();
1001 if (mIncludedSubnetsv4.size() > 0)
1002 {
1003 ranges.add(mIncludedSubnetsv4);
1004 }
1005 else
1006 {
1007 ranges.addAll(mRoutesIPv4);
1008 }
1009 ranges.remove(mExcludedSubnets);
1010 for (IPRange subnet : ranges.subnets())
1011 {
1012 try
1013 {
1014 builder.addRoute(subnet.getFrom(), subnet.getPrefix());
1015 }
1016 catch (IllegalArgumentException e)
1017 { /* some Android versions don't seem to like multicast addresses here,
1018 * ignore it for now */
1019 if (!subnet.getFrom().isMulticastAddress())
1020 {
1021 throw e;
1022 }
1023 }
1024 }
1025 }
1026 else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
1027 { /* allow traffic that would otherwise be blocked to bypass the VPN */
1028 builder.allowFamily(OsConstants.AF_INET);
1029 }
1030 }
1031 else if (mIPv4Seen)
1032 { /* only needed if we've seen any addresses. otherwise, traffic
1033 * is blocked by default (we also install no routes in that case) */
1034 builder.addRoute("0.0.0.0", 0);
1035 }
1036 /* same thing for IPv6 */
1037 if ((mSplitTunneling & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) == 0)
1038 {
1039 if (mIPv6Seen)
1040 {
1041 IPRangeSet ranges = new IPRangeSet();
1042 if (mIncludedSubnetsv6.size() > 0)
1043 {
1044 ranges.add(mIncludedSubnetsv6);
1045 }
1046 else
1047 {
1048 ranges.addAll(mRoutesIPv6);
1049 }
1050 ranges.remove(mExcludedSubnets);
1051 for (IPRange subnet : ranges.subnets())
1052 {
1053 try
1054 {
1055 builder.addRoute(subnet.getFrom(), subnet.getPrefix());
1056 }
1057 catch (IllegalArgumentException e)
1058 {
1059 if (!subnet.getFrom().isMulticastAddress())
1060 {
1061 throw e;
1062 }
1063 }
1064 }
1065 }
1066 else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
1067 {
1068 builder.allowFamily(OsConstants.AF_INET6);
1069 }
1070 }
1071 else if (mIPv6Seen)
1072 {
1073 builder.addRoute("::", 0);
1074 }
1075 /* apply selected applications */
1076 if (mSelectedApps.size() > 0 &&
1077 Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
1078 {
1079 switch (mAppHandling)
1080 {
1081 case SELECTED_APPS_EXCLUDE:
1082 for (String app : mSelectedApps)
1083 {
1084 try
1085 {
1086 builder.addDisallowedApplication(app);
1087 }
1088 catch (PackageManager.NameNotFoundException e)
1089 {
1090 // possible if not configured via GUI or app was uninstalled
1091 }
1092 }
1093 break;
1094 case SELECTED_APPS_ONLY:
1095 for (String app : mSelectedApps)
1096 {
1097 try
1098 {
1099 builder.addAllowedApplication(app);
1100 }
1101 catch (PackageManager.NameNotFoundException e)
1102 {
1103 // possible if not configured via GUI or app was uninstalled
1104 }
1105 }
1106 break;
1107 default:
1108 break;
1109 }
1110 }
1111 builder.setMtu(mMtu);
1112 }
1113
1114 private boolean isIPv6(String address) throws UnknownHostException
1115 {
1116 InetAddress addr = InetAddress.getByName(address);
1117 if (addr instanceof Inet4Address)
1118 {
1119 return false;
1120 }
1121 else if (addr instanceof Inet6Address)
1122 {
1123 return true;
1124 }
1125 return false;
1126 }
1127 }
1128
1129 /**
1130 * Function called via JNI to determine information about the Android version.
1131 */
1132 private static String getAndroidVersion()
1133 {
1134 String version = "Android " + Build.VERSION.RELEASE + " - " + Build.DISPLAY;
1135 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
1136 {
1137 version += "/" + Build.VERSION.SECURITY_PATCH;
1138 }
1139 return version;
1140 }
1141
1142 /**
1143 * Function called via JNI to determine information about the device.
1144 */
1145 private static String getDeviceString()
1146 {
1147 return Build.MODEL + " - " + Build.BRAND + "/" + Build.PRODUCT + "/" + Build.MANUFACTURER;
1148 }
1149 }