2 * Copyright (C) 2012-2025 Tobias Brunner
3 * Copyright (C) 2012 Giuliano Grassi
4 * Copyright (C) 2012 Ralf Sager
6 * Copyright (C) secunet Security Networks AG
8 * This program is free software; you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License as published by the
10 * Free Software Foundation; either version 2 of the License, or (at your
11 * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
13 * This program is distributed in the hope that it will be useful, but
14 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
19 package org
.strongswan
.android
.logic
;
21 import android
.annotation
.TargetApi
;
22 import android
.app
.Notification
;
23 import android
.app
.NotificationChannel
;
24 import android
.app
.NotificationManager
;
25 import android
.app
.PendingIntent
;
26 import android
.app
.Service
;
27 import android
.content
.ComponentName
;
28 import android
.content
.Context
;
29 import android
.content
.Intent
;
30 import android
.content
.ServiceConnection
;
31 import android
.content
.SharedPreferences
;
32 import android
.content
.pm
.PackageManager
;
33 import android
.net
.ProxyInfo
;
34 import android
.net
.VpnService
;
35 import android
.os
.Build
;
36 import android
.os
.Bundle
;
37 import android
.os
.Handler
;
38 import android
.os
.IBinder
;
39 import android
.os
.ParcelFileDescriptor
;
40 import android
.security
.KeyChain
;
41 import android
.security
.KeyChainException
;
42 import android
.system
.OsConstants
;
43 import android
.util
.Log
;
45 import org
.strongswan
.android
.R
;
46 import org
.strongswan
.android
.data
.VpnProfile
;
47 import org
.strongswan
.android
.data
.VpnProfile
.SelectedAppsHandling
;
48 import org
.strongswan
.android
.data
.VpnProfileDataSource
;
49 import org
.strongswan
.android
.data
.VpnProfileSource
;
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 import org
.strongswan
.android
.utils
.Utils
;
64 import java
.io
.FileInputStream
;
65 import java
.io
.IOException
;
66 import java
.net
.Inet4Address
;
67 import java
.net
.Inet6Address
;
68 import java
.net
.InetAddress
;
69 import java
.net
.UnknownHostException
;
70 import java
.nio
.ByteBuffer
;
71 import java
.nio
.channels
.ClosedByInterruptException
;
72 import java
.security
.PrivateKey
;
73 import java
.security
.cert
.CertificateEncodingException
;
74 import java
.security
.cert
.X509Certificate
;
75 import java
.util
.ArrayList
;
76 import java
.util
.Arrays
;
77 import java
.util
.Collections
;
78 import java
.util
.List
;
79 import java
.util
.Locale
;
80 import java
.util
.SortedSet
;
82 import androidx
.core
.app
.NotificationCompat
;
83 import androidx
.core
.content
.ContextCompat
;
84 import androidx
.preference
.PreferenceManager
;
86 public class CharonVpnService
extends VpnService
implements Runnable
, VpnStateService
.VpnStateListener
88 private static final String TAG
= CharonVpnService
.class.getSimpleName();
89 private static final String VPN_SERVICE_ACTION
= "android.net.VpnService";
90 public static final String DISCONNECT_ACTION
= "org.strongswan.android.CharonVpnService.DISCONNECT";
91 private static final String NOTIFICATION_CHANNEL
= "org.strongswan.android.CharonVpnService.VPN_STATE_NOTIFICATION";
92 public static final String LOG_FILE
= "charon.log";
93 public static final String KEY_IS_RETRY
= "retry";
94 public static final int VPN_STATE_NOTIFICATION_ID
= 1;
96 private String mLogFile
;
97 private String mAppDir
;
98 private VpnProfileDataSource mDataSource
;
99 private Thread mConnectionHandler
;
100 private VpnProfile mCurrentProfile
;
101 private volatile String mCurrentCertificateAlias
;
102 private volatile String mCurrentUserCertificateAlias
;
103 private VpnProfile mNextProfile
;
104 private volatile boolean mProfileUpdated
;
105 private volatile boolean mTerminate
;
106 private volatile boolean mIsDisconnecting
;
107 private volatile boolean mShowNotification
;
108 private final BuilderAdapter mBuilderAdapter
= new BuilderAdapter();
109 private Handler mHandler
;
110 private VpnStateService mService
;
111 private final Object mServiceLock
= new Object();
112 private final ServiceConnection mServiceConnection
= new ServiceConnection()
115 public void onServiceDisconnected(ComponentName name
)
116 { /* since the service is local this is theoretically only called when the process is terminated */
117 synchronized (mServiceLock
)
124 public void onServiceConnected(ComponentName name
, IBinder service
)
126 synchronized (mServiceLock
)
128 mService
= ((VpnStateService
.LocalBinder
)service
).getService();
130 /* we are now ready to start the handler thread */
131 mService
.registerListener(CharonVpnService
.this);
132 mConnectionHandler
.start();
137 * as defined in charonservice.h
139 static final int STATE_CHILD_SA_UP
= 1;
140 static final int STATE_CHILD_SA_DOWN
= 2;
141 static final int STATE_AUTH_ERROR
= 3;
142 static final int STATE_PEER_AUTH_ERROR
= 4;
143 static final int STATE_LOOKUP_ERROR
= 5;
144 static final int STATE_UNREACHABLE_ERROR
= 6;
145 static final int STATE_CERTIFICATE_UNAVAILABLE
= 7;
146 static final int STATE_GENERIC_ERROR
= 8;
149 public int onStartCommand(Intent intent
, int flags
, int startId
)
153 VpnProfile profile
= null;
154 boolean retry
= false;
156 if (VPN_SERVICE_ACTION
.equals(intent
.getAction()))
157 { /* triggered when Always-on VPN is activated */
158 SharedPreferences pref
= PreferenceManager
.getDefaultSharedPreferences(this);
159 String uuid
= pref
.getString(Constants
.PREF_DEFAULT_VPN_PROFILE
, null);
160 if (uuid
== null || uuid
.equals(Constants
.PREF_DEFAULT_VPN_PROFILE_MRU
))
162 uuid
= pref
.getString(Constants
.PREF_MRU_VPN_PROFILE
, null);
164 profile
= mDataSource
.getVpnProfile(uuid
);
166 else if (!DISCONNECT_ACTION
.equals(intent
.getAction()))
168 Bundle bundle
= intent
.getExtras();
171 profile
= mDataSource
.getVpnProfile(bundle
.getString(VpnProfileDataSource
.KEY_UUID
));
174 String password
= bundle
.getString(VpnProfileDataSource
.KEY_PASSWORD
);
175 profile
.setPassword(password
);
177 retry
= bundle
.getBoolean(CharonVpnService
.KEY_IS_RETRY
, false);
179 SharedPreferences pref
= PreferenceManager
.getDefaultSharedPreferences(this);
180 pref
.edit().putString(Constants
.PREF_MRU_VPN_PROFILE
, profile
.getUUID().toString())
185 if (profile
!= null && !retry
)
186 { /* delete the log file if this is not an automatic retry */
187 deleteFile(LOG_FILE
);
189 setNextProfile(profile
);
191 return START_NOT_STICKY
;
195 public void onCreate()
197 mLogFile
= getFilesDir().getAbsolutePath() + File
.separator
+ LOG_FILE
;
198 mAppDir
= getFilesDir().getAbsolutePath();
200 /* handler used to do changes in the main UI thread */
201 mHandler
= new Handler(getMainLooper());
203 mDataSource
= new VpnProfileSource(this);
205 /* use a separate thread as main thread for charon */
206 mConnectionHandler
= new Thread(this);
207 /* the thread is started when the service is bound */
208 bindService(new Intent(this, VpnStateService
.class),
209 mServiceConnection
, Service
.BIND_AUTO_CREATE
);
211 createNotificationChannel();
215 public void onRevoke()
216 { /* the system revoked the rights grated with the initial prepare() call.
217 * called when the user clicks disconnect in the system's VPN dialog */
218 setNextProfile(null);
222 public void onDestroy()
225 setNextProfile(null);
228 mConnectionHandler
.join();
230 catch (InterruptedException e
)
234 if (mService
!= null)
236 mService
.unregisterListener(this);
237 unbindService(mServiceConnection
);
243 * Set the profile that is to be initiated next. Notify the handler thread.
245 * @param profile the profile to initiate
247 private void setNextProfile(VpnProfile profile
)
251 this.mNextProfile
= profile
;
252 mProfileUpdated
= true;
266 while (!mProfileUpdated
)
271 mProfileUpdated
= false;
272 stopCurrentConnection();
273 if (mNextProfile
== null)
275 setState(State
.DISABLED
);
283 mCurrentProfile
= mNextProfile
;
286 /* store this in a separate (volatile) variable to avoid
287 * a possible deadlock during deinitialization */
288 mCurrentCertificateAlias
= mCurrentProfile
.getCertificateAlias();
289 mCurrentUserCertificateAlias
= mCurrentProfile
.getUserCertificateAlias();
291 startConnection(mCurrentProfile
);
292 mIsDisconnecting
= false;
294 SimpleFetcher
.enable();
296 mBuilderAdapter
.setProfile(mCurrentProfile
);
297 if (initializeCharon(mBuilderAdapter
, mLogFile
, mAppDir
, mCurrentProfile
.getVpnType().has(VpnTypeFeature
.BYOD
),
298 (mCurrentProfile
.getFlags() & VpnProfile
.FLAGS_IPv6_TRANSPORT
) != 0))
300 Log
.i(TAG
, "charon started");
302 if (mCurrentProfile
.getVpnType().has(VpnTypeFeature
.USER_PASS
) &&
303 mCurrentProfile
.getPassword() == null)
304 { /* this can happen if Always-on VPN is enabled with an incomplete profile */
305 setError(ErrorState
.PASSWORD_MISSING
);
309 SettingsWriter writer
= new SettingsWriter();
310 writer
.setValue("global.language", Locale
.getDefault().getLanguage());
311 writer
.setValue("global.mtu", mCurrentProfile
.getMTU());
312 writer
.setValue("global.nat_keepalive", mCurrentProfile
.getNATKeepAlive());
313 writer
.setValue("global.rsa_pss", (mCurrentProfile
.getFlags() & VpnProfile
.FLAGS_RSA_PSS
) != 0);
314 writer
.setValue("global.crl", (mCurrentProfile
.getFlags() & VpnProfile
.FLAGS_DISABLE_CRL
) == 0);
315 writer
.setValue("global.ocsp", (mCurrentProfile
.getFlags() & VpnProfile
.FLAGS_DISABLE_OCSP
) == 0);
316 writer
.setValue("connection.type", mCurrentProfile
.getVpnType().getIdentifier());
317 writer
.setValue("connection.server", mCurrentProfile
.getGateway());
318 writer
.setValue("connection.port", mCurrentProfile
.getPort());
319 writer
.setValue("connection.username", mCurrentProfile
.getUsername());
320 writer
.setValue("connection.password", mCurrentProfile
.getPassword());
321 writer
.setValue("connection.local_id", mCurrentProfile
.getLocalId());
322 writer
.setValue("connection.remote_id", mCurrentProfile
.getRemoteId());
323 writer
.setValue("connection.certreq", (mCurrentProfile
.getFlags() & VpnProfile
.FLAGS_SUPPRESS_CERT_REQS
) == 0);
324 writer
.setValue("connection.strict_revocation", (mCurrentProfile
.getFlags() & VpnProfile
.FLAGS_STRICT_REVOCATION
) != 0);
325 writer
.setValue("connection.ike_proposal", mCurrentProfile
.getIkeProposal());
326 writer
.setValue("connection.esp_proposal", mCurrentProfile
.getEspProposal());
327 initiate(writer
.serialize());
331 Log
.e(TAG
, "failed to start charon");
332 setError(ErrorState
.GENERIC_ERROR
);
333 setState(State
.DISABLED
);
334 mCurrentProfile
= null;
338 catch (InterruptedException ex
)
340 stopCurrentConnection();
341 setState(State
.DISABLED
);
348 * Stop any existing connection by deinitializing charon.
350 private void stopCurrentConnection()
354 if (mNextProfile
!= null)
356 mBuilderAdapter
.setProfile(mNextProfile
);
357 mBuilderAdapter
.establishBlocking();
360 if (mCurrentProfile
!= null)
362 setState(State
.DISCONNECTING
);
363 mIsDisconnecting
= true;
364 SimpleFetcher
.disable();
365 deinitializeCharon();
366 Log
.i(TAG
, "charon stopped");
367 mCurrentProfile
= null;
368 if (mNextProfile
== null)
369 { /* only do this if we are not connecting to another profile */
370 removeNotification();
371 mBuilderAdapter
.closeBlocking();
378 * Add a permanent notification while we are connected to avoid the service getting killed by
379 * the system when low on memory.
381 private void addNotification()
383 mHandler
.post(new Runnable()
388 mShowNotification
= true;
389 startForeground(VPN_STATE_NOTIFICATION_ID
, buildNotification(false));
395 * Remove the permanent notification.
397 private void removeNotification()
399 mHandler
.post(new Runnable()
404 mShowNotification
= false;
405 if (Build
.VERSION
.SDK_INT
< Build
.VERSION_CODES
.N
)
407 stopForegroundCompat();
411 stopForeground(STOP_FOREGROUND_REMOVE
);
417 @SuppressWarnings("deprecation")
418 private void stopForegroundCompat()
420 stopForeground(true);
424 * Create a notification channel for Android 8+
426 private void createNotificationChannel()
428 if (Build
.VERSION
.SDK_INT
>= Build
.VERSION_CODES
.O
)
430 NotificationChannel channel
;
431 channel
= new NotificationChannel(NOTIFICATION_CHANNEL
, getString(R
.string
.permanent_notification_name
),
432 NotificationManager
.IMPORTANCE_LOW
);
433 channel
.setDescription(getString(R
.string
.permanent_notification_description
));
434 channel
.setLockscreenVisibility(Notification
.VISIBILITY_SECRET
);
435 channel
.setShowBadge(false);
436 NotificationManager notificationManager
= getSystemService(NotificationManager
.class);
437 notificationManager
.createNotificationChannel(channel
);
443 * Build a notification matching the current state
445 private Notification
buildNotification(boolean publicVersion
)
447 VpnProfile profile
= mService
.getProfile();
448 State state
= mService
.getState();
449 ErrorState error
= mService
.getErrorState();
451 boolean add_action
= false;
455 name
= profile
.getName();
457 NotificationCompat
.Builder builder
= new NotificationCompat
.Builder(this, NOTIFICATION_CHANNEL
)
458 .setSmallIcon(R
.drawable
.ic_notification
)
459 .setCategory(NotificationCompat
.CATEGORY_SERVICE
)
460 .setVisibility(publicVersion ? NotificationCompat
.VISIBILITY_PUBLIC
461 : NotificationCompat
.VISIBILITY_PRIVATE
);
462 int s
= R
.string
.state_disabled
;
463 if (error
!= ErrorState
.NO_ERROR
)
465 s
= mService
.getErrorText();
466 builder
.setSmallIcon(R
.drawable
.ic_notification_warning
);
467 builder
.setColor(ContextCompat
.getColor(this, R
.color
.error_text
));
469 if (!publicVersion
&& profile
!= null)
471 int retry
= mService
.getRetryIn();
474 builder
.setContentText(getResources().getQuantityString(R
.plurals
.retry_in
, retry
, retry
));
475 builder
.setProgress(mService
.getRetryTimeout(), retry
, false);
478 Intent intent
= new Intent(getApplicationContext(), VpnProfileControlActivity
.class);
479 intent
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
);
480 intent
.setAction(VpnProfileControlActivity
.START_PROFILE
);
481 intent
.putExtra(VpnProfileControlActivity
.EXTRA_VPN_PROFILE_UUID
, profile
.getUUID().toString());
482 int flags
= PendingIntent
.FLAG_UPDATE_CURRENT
;
483 if (Build
.VERSION
.SDK_INT
>= Build
.VERSION_CODES
.M
)
485 flags
|= PendingIntent
.FLAG_IMMUTABLE
;
487 PendingIntent pending
= PendingIntent
.getActivity(getApplicationContext(), 0, intent
,
489 builder
.addAction(R
.drawable
.ic_notification_connecting
, getString(R
.string
.retry
), pending
);
495 builder
.setProgress(0, 0, false);
500 s
= R
.string
.state_connecting
;
501 builder
.setSmallIcon(R
.drawable
.ic_notification_connecting
);
502 builder
.setColor(ContextCompat
.getColor(this, R
.color
.warning_text
));
506 s
= R
.string
.state_connected
;
507 builder
.setColor(ContextCompat
.getColor(this, R
.color
.success_text
));
508 builder
.setUsesChronometer(true);
512 s
= R
.string
.state_disconnecting
;
516 builder
.setContentTitle(getString(s
));
518 int flags
= PendingIntent
.FLAG_UPDATE_CURRENT
;
519 if (Build
.VERSION
.SDK_INT
>= Build
.VERSION_CODES
.M
)
521 flags
|= PendingIntent
.FLAG_IMMUTABLE
;
527 Intent intent
= new Intent(getApplicationContext(), VpnProfileControlActivity
.class);
528 intent
.setAction(VpnProfileControlActivity
.DISCONNECT
);
529 PendingIntent pending
= PendingIntent
.getActivity(getApplicationContext(), 0, intent
,
531 builder
.addAction(R
.drawable
.ic_notification_disconnect
, getString(R
.string
.disconnect
), pending
);
533 if (error
== ErrorState
.NO_ERROR
)
535 builder
.setContentText(name
);
537 builder
.setPublicVersion(buildNotification(true));
540 Intent intent
= new Intent(getApplicationContext(), MainActivity
.class);
541 PendingIntent pending
= PendingIntent
.getActivity(getApplicationContext(), 0, intent
,
543 builder
.setContentIntent(pending
);
544 return builder
.build();
548 public void stateChanged()
550 if (mShowNotification
)
552 NotificationManager manager
= (NotificationManager
)getSystemService(Context
.NOTIFICATION_SERVICE
);
553 manager
.notify(VPN_STATE_NOTIFICATION_ID
, buildNotification(false));
558 * Notify the state service about a new connection attempt.
559 * Called by the handler thread.
561 * @param profile currently active VPN profile
563 private void startConnection(VpnProfile profile
)
565 synchronized (mServiceLock
)
567 if (mService
!= null)
569 mService
.startConnection(profile
);
575 * Update the current VPN state on the state service. Called by the handler
576 * thread and any of charon's threads.
578 * @param state current state
580 private void setState(State state
)
582 synchronized (mServiceLock
)
584 if (mService
!= null)
586 mService
.setState(state
);
592 * Set an error on the state service. Called by the handler thread and any
593 * of charon's threads.
595 * @param error error state
597 private void setError(ErrorState error
)
599 synchronized (mServiceLock
)
601 if (mService
!= null)
603 mService
.setError(error
);
609 * Set the IMC state on the state service. Called by the handler thread and
610 * any of charon's threads.
612 * @param state IMC state
614 private void setImcState(ImcState state
)
616 synchronized (mServiceLock
)
618 if (mService
!= null)
620 mService
.setImcState(state
);
626 * Set an error on the state service. Called by the handler thread and any
627 * of charon's threads.
629 * @param error error state
631 private void setErrorDisconnect(ErrorState error
)
633 synchronized (mServiceLock
)
635 if (mService
!= null)
637 if (!mIsDisconnecting
)
639 mService
.setError(error
);
646 * Updates the state of the current connection.
647 * Called via JNI by different threads (but not concurrently).
649 * @param status new state
651 public void updateStatus(int status
)
655 case STATE_CHILD_SA_DOWN
:
656 if (!mIsDisconnecting
)
658 setState(State
.CONNECTING
);
661 case STATE_CHILD_SA_UP
:
662 setState(State
.CONNECTED
);
664 case STATE_AUTH_ERROR
:
665 setErrorDisconnect(ErrorState
.AUTH_FAILED
);
667 case STATE_PEER_AUTH_ERROR
:
668 setErrorDisconnect(ErrorState
.PEER_AUTH_FAILED
);
670 case STATE_LOOKUP_ERROR
:
671 setErrorDisconnect(ErrorState
.LOOKUP_FAILED
);
673 case STATE_UNREACHABLE_ERROR
:
674 setErrorDisconnect(ErrorState
.UNREACHABLE
);
676 case STATE_CERTIFICATE_UNAVAILABLE
:
677 setErrorDisconnect(ErrorState
.CERTIFICATE_UNAVAILABLE
);
679 case STATE_GENERIC_ERROR
:
680 setErrorDisconnect(ErrorState
.GENERIC_ERROR
);
683 Log
.e(TAG
, "Unknown status code received");
689 * Updates the IMC state of the current connection.
690 * Called via JNI by different threads (but not concurrently).
692 * @param value new state
694 public void updateImcState(int value
)
696 ImcState state
= ImcState
.fromValue(value
);
704 * Add a remediation instruction to the VPN state service.
705 * Called via JNI by different threads (but not concurrently).
707 * @param xml XML text
709 public void addRemediationInstruction(String xml
)
711 for (RemediationInstruction instruction
: RemediationInstruction
.fromXml(xml
))
713 synchronized (mServiceLock
)
715 if (mService
!= null)
717 mService
.addRemediationInstruction(instruction
);
724 * Function called via JNI to generate a list of DER encoded CA certificates
727 * @return a list of DER encoded CA certificates
729 private byte[][] getTrustedCertificates()
731 ArrayList
<byte[]> certs
= new ArrayList
<byte[]>();
732 TrustedCertificateManager certman
= TrustedCertificateManager
.getInstance().load();
735 String alias
= this.mCurrentCertificateAlias
;
738 X509Certificate cert
= certman
.getCACertificateFromAlias(alias
);
743 certs
.add(cert
.getEncoded());
747 for (X509Certificate cert
: certman
.getAllCACertificates().values())
749 certs
.add(cert
.getEncoded());
753 catch (CertificateEncodingException e
)
758 return certs
.toArray(new byte[certs
.size()][]);
762 * Function called via JNI to get a list containing the DER encoded certificates
763 * of the user selected certificate chain (beginning with the user certificate).
765 * Since this method is called from a thread of charon's thread pool we are safe
766 * to call methods on KeyChain directly.
768 * @return list containing the certificates (first element is the user certificate)
769 * @throws InterruptedException
770 * @throws KeyChainException
771 * @throws CertificateEncodingException
773 private byte[][] getUserCertificate() throws KeyChainException
, InterruptedException
, CertificateEncodingException
775 ArrayList
<byte[]> encodings
= new ArrayList
<byte[]>();
776 X509Certificate
[] chain
= KeyChain
.getCertificateChain(getApplicationContext(), mCurrentUserCertificateAlias
);
777 if (chain
== null || chain
.length
== 0)
781 for (X509Certificate cert
: chain
)
783 encodings
.add(cert
.getEncoded());
785 return encodings
.toArray(new byte[encodings
.size()][]);
789 * Function called via JNI to get the private key the user selected.
791 * Since this method is called from a thread of charon's thread pool we are safe
792 * to call methods on KeyChain directly.
794 * @return the private key
795 * @throws InterruptedException
796 * @throws KeyChainException
798 private PrivateKey
getUserKey() throws KeyChainException
, InterruptedException
800 return KeyChain
.getPrivateKey(getApplicationContext(), mCurrentUserCertificateAlias
);
804 * Initialization of charon, provided by libandroidbridge.so
806 * @param builder BuilderAdapter for this connection
807 * @param logfile absolute path to the logfile
808 * @param appdir absolute path to the data directory of the app
809 * @param byod enable BYOD features
810 * @param ipv6 enable IPv6 transport
811 * @return TRUE if initialization was successful
813 public native boolean initializeCharon(BuilderAdapter builder
, String logfile
, String appdir
, boolean byod
, boolean ipv6
);
816 * Deinitialize charon, provided by libandroidbridge.so
818 public native void deinitializeCharon();
821 * Initiate VPN, provided by libandroidbridge.so
823 public native void initiate(String config
);
826 * Adapter for VpnService.Builder which is used to access it safely via JNI.
827 * There is a corresponding C object to access it from native code.
829 public class BuilderAdapter
831 private VpnProfile mProfile
;
832 private VpnService
.Builder mBuilder
;
833 private BuilderCache mCache
;
834 private BuilderCache mEstablishedCache
;
835 private final PacketDropper mDropper
= new PacketDropper();
837 public synchronized void setProfile(VpnProfile profile
)
840 mBuilder
= createBuilder(mProfile
.getName());
841 mCache
= new BuilderCache(mProfile
);
844 private VpnService
.Builder
createBuilder(String name
)
846 VpnService
.Builder builder
= new CharonVpnService
.Builder();
847 builder
.setSession(name
);
849 /* even though the option displayed in the system dialog says "Configure"
850 * we just use our main Activity */
851 Context context
= getApplicationContext();
852 Intent intent
= new Intent(context
, MainActivity
.class);
853 int flags
= PendingIntent
.FLAG_UPDATE_CURRENT
;
854 if (Build
.VERSION
.SDK_INT
>= Build
.VERSION_CODES
.M
)
856 flags
|= PendingIntent
.FLAG_IMMUTABLE
;
858 PendingIntent pending
= PendingIntent
.getActivity(context
, 0, intent
, flags
);
859 builder
.setConfigureIntent(pending
);
861 /* mark all VPN connections as unmetered (default changed for Android 10) */
862 if (Build
.VERSION
.SDK_INT
>= Build
.VERSION_CODES
.Q
)
864 builder
.setMetered(false);
869 public synchronized boolean addAddress(String address
, int prefixLength
)
873 mCache
.addAddress(address
, prefixLength
);
875 catch (IllegalArgumentException ex
)
882 public synchronized boolean addDnsServer(String address
)
886 mCache
.addDnsServer(address
);
888 catch (IllegalArgumentException ex
)
895 public synchronized boolean addRoute(String address
, int prefixLength
)
899 mCache
.addRoute(address
, prefixLength
);
901 catch (IllegalArgumentException ex
)
908 public synchronized boolean addSearchDomain(String domain
)
912 mBuilder
.addSearchDomain(domain
);
914 catch (IllegalArgumentException ex
)
921 public synchronized boolean setMtu(int mtu
)
927 catch (IllegalArgumentException ex
)
934 private synchronized ParcelFileDescriptor
establishIntern()
936 ParcelFileDescriptor fd
;
939 mCache
.applyData(mBuilder
);
940 fd
= mBuilder
.establish();
948 ex
.printStackTrace();
955 /* now that the TUN device is created we don't need the current
956 * builder anymore, but we might need another when reestablishing */
957 mBuilder
= createBuilder(mProfile
.getName());
958 mEstablishedCache
= mCache
;
959 mCache
= new BuilderCache(mProfile
);
963 public synchronized int establish()
965 ParcelFileDescriptor fd
= establishIntern();
966 return fd
!= null ? fd
.detachFd() : -1;
969 @TargetApi(Build
.VERSION_CODES
.LOLLIPOP
)
970 public synchronized void establishBlocking()
972 /* just choose some arbitrary values to block all traffic (except for what's configured in the profile) */
973 mCache
.addAddress("172.16.252.1", 32);
974 mCache
.addAddress("fd00::fd02:1", 128);
975 mCache
.addRoute("0.0.0.0", 0);
976 mCache
.addRoute("::", 0);
977 /* set DNS servers to avoid DNS leak later */
978 mBuilder
.addDnsServer("8.8.8.8");
979 mBuilder
.addDnsServer("2001:4860:4860::8888");
980 /* use blocking mode to simplify packet dropping */
981 mBuilder
.setBlocking(true);
982 ParcelFileDescriptor fd
= establishIntern();
989 public synchronized void closeBlocking()
994 public synchronized int establishNoDns()
996 ParcelFileDescriptor fd
;
998 if (mEstablishedCache
== null)
1004 Builder builder
= createBuilder(mProfile
.getName());
1005 mEstablishedCache
.applyData(builder
);
1006 fd
= builder
.establish();
1008 catch (Exception ex
)
1010 ex
.printStackTrace();
1017 return fd
.detachFd();
1020 private class PacketDropper
implements Runnable
1022 private ParcelFileDescriptor mFd
;
1023 private Thread mThread
;
1025 public void start(ParcelFileDescriptor fd
)
1028 mThread
= new Thread(this);
1038 mThread
.interrupt();
1042 catch (InterruptedException e
)
1044 e
.printStackTrace();
1046 catch (IOException e
)
1048 e
.printStackTrace();
1055 public synchronized void run()
1057 try (FileInputStream plain
= new FileInputStream(mFd
.getFileDescriptor()))
1059 ByteBuffer packet
= ByteBuffer
.allocate(mCache
.mMtu
);
1062 if (Build
.VERSION
.SDK_INT
>= Build
.VERSION_CODES
.N
)
1063 { /* just read and ignore all data, regular read() is not interruptible */
1064 int len
= plain
.getChannel().read(packet
);
1072 { /* this is rather ugly but on older platforms not even the NIO version of read() is interruptible */
1073 boolean wait
= true;
1074 if (plain
.available() > 0)
1076 int len
= plain
.read(packet
.array());
1078 if (len
< 0 || Thread
.interrupted())
1082 /* check again right away, there may be another packet */
1092 catch (final ClosedByInterruptException
| InterruptedException e
)
1094 /* regular interruption */
1096 catch (IOException e
)
1098 e
.printStackTrace();
1105 * Cache non DNS related information so we can recreate the builder without
1106 * that information when reestablishing IKE_SAs
1108 public class BuilderCache
1110 private final List
<IPRange
> mAddresses
= new ArrayList
<>();
1111 private final List
<IPRange
> mRoutesIPv4
= new ArrayList
<>();
1112 private final List
<IPRange
> mRoutesIPv6
= new ArrayList
<>();
1113 private final IPRangeSet mIncludedSubnetsv4
= new IPRangeSet();
1114 private final IPRangeSet mIncludedSubnetsv6
= new IPRangeSet();
1115 private final IPRangeSet mExcludedSubnets
;
1116 private final int mSplitTunneling
;
1117 private final SelectedAppsHandling mAppHandling
;
1118 private final SortedSet
<String
> mSelectedApps
;
1119 private final List
<InetAddress
> mDnsServers
= new ArrayList
<>();
1121 private boolean mIPv4Seen
, mIPv6Seen
, mDnsServersConfigured
;
1122 private ProxyInfo mProxyServer
;
1124 public BuilderCache(VpnProfile profile
)
1126 IPRangeSet included
= IPRangeSet
.fromString(profile
.getIncludedSubnets());
1127 for (IPRange range
: included
)
1129 if (range
.getFrom() instanceof Inet4Address
)
1131 mIncludedSubnetsv4
.add(range
);
1133 else if (range
.getFrom() instanceof Inet6Address
)
1135 mIncludedSubnetsv6
.add(range
);
1138 mExcludedSubnets
= IPRangeSet
.fromString(profile
.getExcludedSubnets());
1139 Integer splitTunneling
= profile
.getSplitTunneling();
1140 mSplitTunneling
= splitTunneling
!= null ? splitTunneling
: 0;
1141 SelectedAppsHandling appHandling
= profile
.getSelectedAppsHandling();
1142 mSelectedApps
= profile
.getSelectedAppsSet();
1143 /* exclude our own app, otherwise the fetcher is blocked */
1144 switch (appHandling
)
1146 case SELECTED_APPS_DISABLE
:
1147 appHandling
= SelectedAppsHandling
.SELECTED_APPS_EXCLUDE
;
1148 mSelectedApps
.clear();
1150 case SELECTED_APPS_EXCLUDE
:
1151 mSelectedApps
.add(getPackageName());
1153 case SELECTED_APPS_ONLY
:
1154 mSelectedApps
.remove(getPackageName());
1157 mAppHandling
= appHandling
;
1159 if (profile
.getDnsServers() != null)
1161 for (String server
: profile
.getDnsServers().split("\\s+"))
1165 mDnsServers
.add(Utils
.parseInetAddress(server
));
1166 recordAddressFamily(server
);
1167 mDnsServersConfigured
= true;
1169 catch (UnknownHostException e
)
1171 e
.printStackTrace();
1176 if (profile
.getProxyHost() != null)
1178 int port
= profile
.getProxyPort() != null ? profile
.getProxyPort() : Constants
.PROXY_PORT_DEFAULT
;
1179 List
<String
> exclusions
= new ArrayList
<>();
1180 if (profile
.getProxyExclusions() != null)
1182 Collections
.addAll(exclusions
, profile
.getProxyExclusions().split("\\s+"));
1184 mProxyServer
= ProxyInfo
.buildDirectProxy(profile
.getProxyHost(), port
, exclusions
);
1187 /* set a default MTU, will be set by the daemon for regular interfaces */
1188 Integer mtu
= profile
.getMTU();
1189 mMtu
= mtu
== null ? Constants
.MTU_MAX
: mtu
;
1192 public void addAddress(String address
, int prefixLength
)
1196 mAddresses
.add(new IPRange(address
, prefixLength
));
1197 recordAddressFamily(address
);
1199 catch (UnknownHostException ex
)
1201 ex
.printStackTrace();
1205 public void addDnsServer(String address
)
1207 /* ignore received DNS servers if any were configured */
1208 if (mDnsServersConfigured
)
1215 mDnsServers
.add(Utils
.parseInetAddress(address
));
1216 recordAddressFamily(address
);
1218 catch (UnknownHostException e
)
1220 e
.printStackTrace();
1224 public void addRoute(String address
, int prefixLength
)
1228 if (isIPv6(address
))
1230 mRoutesIPv6
.add(new IPRange(address
, prefixLength
));
1234 mRoutesIPv4
.add(new IPRange(address
, prefixLength
));
1237 catch (UnknownHostException ex
)
1239 ex
.printStackTrace();
1243 public void setMtu(int mtu
)
1248 public void recordAddressFamily(String address
)
1252 if (isIPv6(address
))
1261 catch (UnknownHostException ex
)
1263 ex
.printStackTrace();
1267 public void applyData(Builder builder
)
1269 for (IPRange address
: mAddresses
)
1271 builder
.addAddress(address
.getFrom(), address
.getPrefix());
1273 for (InetAddress server
: mDnsServers
)
1275 builder
.addDnsServer(server
);
1277 /* add routes depending on whether split tunneling is allowed or not,
1278 * that is, whether we have to handle and block non-VPN traffic */
1279 if ((mSplitTunneling
& VpnProfile
.SPLIT_TUNNELING_BLOCK_IPV4
) == 0)
1282 { /* split tunneling is used depending on the routes and configuration */
1283 IPRangeSet ranges
= new IPRangeSet();
1284 if (mIncludedSubnetsv4
.size() > 0)
1286 ranges
.add(mIncludedSubnetsv4
);
1290 ranges
.addAll(mRoutesIPv4
);
1292 ranges
.remove(mExcludedSubnets
);
1293 for (IPRange subnet
: ranges
.subnets())
1297 builder
.addRoute(subnet
.getFrom(), subnet
.getPrefix());
1299 catch (IllegalArgumentException e
)
1300 { /* some Android versions don't seem to like multicast addresses here,
1301 * ignore it for now */
1302 if (!subnet
.getFrom().isMulticastAddress())
1310 { /* allow traffic that would otherwise be blocked to bypass the VPN */
1311 builder
.allowFamily(OsConstants
.AF_INET
);
1315 { /* only needed if we've seen any addresses. otherwise, traffic
1316 * is blocked by default (we also install no routes in that case) */
1317 builder
.addRoute("0.0.0.0", 0);
1319 /* same thing for IPv6 */
1320 if ((mSplitTunneling
& VpnProfile
.SPLIT_TUNNELING_BLOCK_IPV6
) == 0)
1324 IPRangeSet ranges
= new IPRangeSet();
1325 if (mIncludedSubnetsv6
.size() > 0)
1327 ranges
.add(mIncludedSubnetsv6
);
1331 ranges
.addAll(mRoutesIPv6
);
1333 ranges
.remove(mExcludedSubnets
);
1334 for (IPRange subnet
: ranges
.subnets())
1338 builder
.addRoute(subnet
.getFrom(), subnet
.getPrefix());
1340 catch (IllegalArgumentException e
)
1342 if (!subnet
.getFrom().isMulticastAddress())
1351 builder
.allowFamily(OsConstants
.AF_INET6
);
1356 builder
.addRoute("::", 0);
1358 /* apply selected applications */
1359 if (mSelectedApps
.size() > 0)
1361 switch (mAppHandling
)
1363 case SELECTED_APPS_EXCLUDE
:
1364 for (String app
: mSelectedApps
)
1368 builder
.addDisallowedApplication(app
);
1370 catch (PackageManager
.NameNotFoundException e
)
1372 // possible if not configured via GUI or app was uninstalled
1376 case SELECTED_APPS_ONLY
:
1377 for (String app
: mSelectedApps
)
1381 builder
.addAllowedApplication(app
);
1383 catch (PackageManager
.NameNotFoundException e
)
1385 // possible if not configured via GUI or app was uninstalled
1393 if (Build
.VERSION
.SDK_INT
>= Build
.VERSION_CODES
.Q
&& mProxyServer
!= null)
1395 builder
.setHttpProxy(mProxyServer
);
1397 builder
.setMtu(mMtu
);
1400 private boolean isIPv6(String address
) throws UnknownHostException
1402 InetAddress addr
= Utils
.parseInetAddress(address
);
1403 if (addr
instanceof Inet4Address
)
1407 return addr
instanceof Inet6Address
;
1412 * Function called via JNI to determine information about the Android version.
1414 private static String
getAndroidVersion()
1416 String version
= "Android " + Build
.VERSION
.RELEASE
+ " - " + Build
.DISPLAY
;
1417 if (Build
.VERSION
.SDK_INT
>= Build
.VERSION_CODES
.M
)
1419 version
+= "/" + Build
.VERSION
.SECURITY_PATCH
;
1425 * Function called via JNI to determine information about the device.
1427 private static String
getDeviceString()
1429 return Build
.MODEL
+ " - " + Build
.BRAND
+ "/" + Build
.PRODUCT
+ "/" + Build
.MANUFACTURER
;