2 * Copyright (C) 2012-2017 Tobias Brunner
4 * Copyright (C) secunet Security Networks AG
6 * This program is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation; either version 2 of the License, or (at your
9 * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
17 package org
.strongswan
.android
.logic
;
19 import android
.app
.Service
;
20 import android
.content
.Context
;
21 import android
.content
.Intent
;
22 import android
.os
.Binder
;
23 import android
.os
.Bundle
;
24 import android
.os
.Handler
;
25 import android
.os
.IBinder
;
26 import android
.os
.Looper
;
27 import android
.os
.Message
;
28 import android
.os
.SystemClock
;
30 import org
.strongswan
.android
.R
;
31 import org
.strongswan
.android
.data
.VpnProfile
;
32 import org
.strongswan
.android
.data
.VpnProfileDataSource
;
33 import org
.strongswan
.android
.data
.VpnType
;
34 import org
.strongswan
.android
.logic
.imc
.ImcState
;
35 import org
.strongswan
.android
.logic
.imc
.RemediationInstruction
;
36 import org
.strongswan
.android
.ui
.VpnProfileControlActivity
;
38 import java
.lang
.ref
.WeakReference
;
39 import java
.util
.Collections
;
40 import java
.util
.HashSet
;
41 import java
.util
.LinkedList
;
42 import java
.util
.List
;
43 import java
.util
.concurrent
.Callable
;
45 import androidx
.core
.content
.ContextCompat
;
47 public class VpnStateService
extends Service
49 private final HashSet
<VpnStateListener
> mListeners
= new HashSet
<VpnStateListener
>();
50 private final IBinder mBinder
= new LocalBinder();
51 private long mConnectionID
= 0;
52 private Handler mHandler
;
53 private VpnProfile mProfile
;
54 private State mState
= State
.DISABLED
;
55 private ErrorState mError
= ErrorState
.NO_ERROR
;
56 private ImcState mImcState
= ImcState
.UNKNOWN
;
57 private final LinkedList
<RemediationInstruction
> mRemediationInstructions
= new LinkedList
<RemediationInstruction
>();
58 private static final long RETRY_INTERVAL
= 1000;
59 /* cap the retry interval at 2 minutes */
60 private static final long MAX_RETRY_INTERVAL
= 120000;
61 private static final int RETRY_MSG
= 1;
62 private final RetryTimeoutProvider mTimeoutProvider
= new RetryTimeoutProvider();
63 private long mRetryTimeout
;
64 private long mRetryIn
;
74 public enum ErrorState
83 CERTIFICATE_UNAVAILABLE
,
87 * Listener interface for bound clients that are interested in changes to
90 public interface VpnStateListener
96 * Simple Binder that allows to directly access this Service class itself
97 * after binding to it.
99 public class LocalBinder
extends Binder
101 public VpnStateService
getService()
103 return VpnStateService
.this;
108 public void onCreate()
110 /* this handler allows us to notify listeners from the UI thread and
111 * not from the threads that actually report any state changes */
112 mHandler
= new RetryHandler(getMainLooper(), this);
116 public IBinder
onBind(Intent intent
)
122 public void onDestroy()
127 * Register a listener with this Service. We assume this is called from
128 * the main thread so no synchronization is happening.
130 * @param listener listener to register
132 public void registerListener(VpnStateListener listener
)
134 mListeners
.add(listener
);
138 * Unregister a listener from this Service.
140 * @param listener listener to unregister
142 public void unregisterListener(VpnStateListener listener
)
144 mListeners
.remove(listener
);
148 * Get the current VPN profile.
152 public VpnProfile
getProfile()
153 { /* only updated from the main thread so no synchronization needed */
158 * Get the current connection ID. May be used to track which state
159 * changes have already been handled.
161 * Is increased when startConnection() is called.
163 * @return connection ID
165 public long getConnectionID()
166 { /* only updated from the main thread so no synchronization needed */
167 return mConnectionID
;
171 * Get the total number of seconds until there is an automatic retry to reconnect.
173 * @return total number of seconds until the retry
175 public int getRetryTimeout()
177 return (int)(mRetryTimeout
/ 1000);
181 * Get the number of seconds until there is an automatic retry to reconnect.
183 * @return number of seconds until the retry
185 public int getRetryIn()
187 return (int)(mRetryIn
/ 1000);
191 * Get the current state.
195 public State
getState()
196 { /* only updated from the main thread so no synchronization needed */
201 * Get the current error, if any.
205 public ErrorState
getErrorState()
206 { /* only updated from the main thread so no synchronization needed */
211 * Get a description of the current error, if any.
213 * @return error description text id
215 public int getErrorText()
220 if (mImcState
== ImcState
.BLOCK
)
222 return R
.string
.error_assessment_failed
;
226 return R
.string
.error_auth_failed
;
228 case PEER_AUTH_FAILED
:
229 return R
.string
.error_peer_auth_failed
;
231 return R
.string
.error_lookup_failed
;
233 return R
.string
.error_unreachable
;
234 case PASSWORD_MISSING
:
235 return R
.string
.error_password_missing
;
236 case CERTIFICATE_UNAVAILABLE
:
237 return R
.string
.error_certificate_unavailable
;
239 return R
.string
.error_generic
;
244 * Get the current IMC state, if any.
248 public ImcState
getImcState()
249 { /* only updated from the main thread so no synchronization needed */
254 * Get the remediation instructions, if any.
256 * @return read-only list of instructions
258 public List
<RemediationInstruction
> getRemediationInstructions()
259 { /* only updated from the main thread so no synchronization needed */
260 return Collections
.unmodifiableList(mRemediationInstructions
);
264 * Disconnect any existing connection and shutdown the daemon, the
265 * VpnService is not stopped but it is reset so new connections can be
268 public void disconnect()
270 /* reset any potential retry timer and error state */
272 setError(ErrorState
.NO_ERROR
);
274 /* as soon as the TUN device is created by calling establish() on the
275 * VpnService.Builder object the system binds to the service and keeps
276 * bound until the file descriptor of the TUN device is closed. thus
277 * calling stopService() here would not stop (destroy) the service yet,
278 * instead we call startService() with a specific action which shuts down
279 * the daemon (and closes the TUN device, if any) */
280 Context context
= getApplicationContext();
281 Intent intent
= new Intent(context
, CharonVpnService
.class);
282 intent
.setAction(CharonVpnService
.DISCONNECT_ACTION
);
283 context
.startService(intent
);
287 * Connect (or reconnect) a profile
289 * @param profileInfo optional profile info (basically the UUID and password), taken from the
290 * previous profile if null
291 * @param fromScratch true if this is a manual retry/reconnect or a completely new connection
293 public void connect(Bundle profileInfo
, boolean fromScratch
)
295 /* we assume we have the necessary permission */
296 Context context
= getApplicationContext();
297 Intent intent
= new Intent(context
, CharonVpnService
.class);
298 if (profileInfo
== null)
300 profileInfo
= new Bundle();
301 profileInfo
.putString(VpnProfileDataSource
.KEY_UUID
, mProfile
.getUUID().toString());
302 /* pass the previous password along */
303 profileInfo
.putString(VpnProfileDataSource
.KEY_PASSWORD
, mProfile
.getPassword());
307 /* reset if this is a manual retry or a new connection */
308 mTimeoutProvider
.reset();
311 { /* mark this as an automatic retry */
312 profileInfo
.putBoolean(CharonVpnService
.KEY_IS_RETRY
, true);
314 intent
.putExtras(profileInfo
);
315 ContextCompat
.startForegroundService(context
, intent
);
319 * Reconnect to the previous profile.
321 public void reconnect()
323 if (mProfile
== null)
327 if (mProfile
.getVpnType().has(VpnType
.VpnTypeFeature
.USER_PASS
))
329 if (mProfile
.getPassword() == null ||
330 mError
== ErrorState
.AUTH_FAILED
)
331 { /* show a dialog if we either don't have the password or if it might be the wrong
332 * one (which is or isn't stored with the profile, let the activity decide) */
333 Intent intent
= new Intent(this, VpnProfileControlActivity
.class);
334 intent
.addFlags(Intent
.FLAG_ACTIVITY_NEW_TASK
);
335 intent
.setAction(VpnProfileControlActivity
.START_PROFILE
);
336 intent
.putExtra(VpnProfileControlActivity
.EXTRA_VPN_PROFILE_UUID
, mProfile
.getUUID().toString());
337 startActivity(intent
);
338 /* reset the retry timer immediately in case the user needs more time to enter the password */
339 notifyListeners(() -> {
350 * Update state and notify all listeners about the change. By using a Handler
351 * this is done from the main UI thread and not the initial reporter thread.
352 * Also, in doing the actual state change from the main thread, listeners
353 * see all changes and none are skipped.
355 * @param change the state update to perform before notifying listeners, returns true if state changed
357 private void notifyListeners(final Callable
<Boolean
> change
)
359 mHandler
.post(new Runnable()
367 { /* otherwise there is no need to notify the listeners */
368 for (VpnStateListener listener
: mListeners
)
370 listener
.stateChanged();
383 * Called when a connection is started. Sets the currently active VPN
384 * profile, resets IMC and Error state variables, sets the State to
385 * CONNECTING, increases the connection ID, and notifies all listeners.
387 * May be called from threads other than the main thread.
389 * @param profile current profile
391 public void startConnection(final VpnProfile profile
)
393 notifyListeners(new Callable
<Boolean
>()
396 public Boolean
call() throws Exception
399 VpnStateService
.this.mConnectionID
++;
400 VpnStateService
.this.mProfile
= profile
;
401 VpnStateService
.this.mState
= State
.CONNECTING
;
402 VpnStateService
.this.mError
= ErrorState
.NO_ERROR
;
403 VpnStateService
.this.mImcState
= ImcState
.UNKNOWN
;
404 VpnStateService
.this.mRemediationInstructions
.clear();
411 * Update the state and notify all listeners, if changed.
413 * May be called from threads other than the main thread.
415 * @param state new state
417 public void setState(final State state
)
419 notifyListeners(new Callable
<Boolean
>()
422 public Boolean
call() throws Exception
424 if (state
== State
.CONNECTED
)
425 { /* reset counter in case there is an error later on */
426 mTimeoutProvider
.reset();
428 if (VpnStateService
.this.mState
!= state
)
430 VpnStateService
.this.mState
= state
;
439 * Set the current error state and notify all listeners, if changed.
441 * May be called from threads other than the main thread.
443 * @param error error state
445 public void setError(final ErrorState error
)
447 notifyListeners(new Callable
<Boolean
>()
450 public Boolean
call() throws Exception
452 if (VpnStateService
.this.mError
!= error
)
454 if (VpnStateService
.this.mError
== ErrorState
.NO_ERROR
)
456 setRetryTimer(error
);
458 else if (error
== ErrorState
.NO_ERROR
)
462 VpnStateService
.this.mError
= error
;
471 * Set the current IMC state and notify all listeners, if changed.
473 * Setting the state to UNKNOWN clears all remediation instructions.
475 * May be called from threads other than the main thread.
477 * @param state IMC state
479 public void setImcState(final ImcState state
)
481 notifyListeners(new Callable
<Boolean
>()
484 public Boolean
call() throws Exception
486 if (state
== ImcState
.UNKNOWN
)
488 VpnStateService
.this.mRemediationInstructions
.clear();
490 if (VpnStateService
.this.mImcState
!= state
)
492 VpnStateService
.this.mImcState
= state
;
501 * Add the given remediation instruction to the internal list. Listeners
504 * Instructions are cleared if the IMC state is set to UNKNOWN.
506 * May be called from threads other than the main thread.
508 * @param instruction remediation instruction
510 public void addRemediationInstruction(final RemediationInstruction instruction
)
512 mHandler
.post(new Runnable()
517 VpnStateService
.this.mRemediationInstructions
.add(instruction
);
523 * Sets the retry timer
525 private void setRetryTimer(ErrorState error
)
527 mRetryTimeout
= mRetryIn
= mTimeoutProvider
.getTimeout(error
);
528 if (mRetryTimeout
<= 0)
532 mHandler
.sendMessageAtTime(mHandler
.obtainMessage(RETRY_MSG
), SystemClock
.uptimeMillis() + RETRY_INTERVAL
);
536 * Reset the retry timer
538 private void resetRetryTimer()
545 * Special Handler subclass that handles the retry countdown (more accurate than CountDownTimer)
547 private static class RetryHandler
extends Handler
549 WeakReference
<VpnStateService
> mService
;
551 public RetryHandler(Looper looper
, VpnStateService service
)
554 mService
= new WeakReference
<>(service
);
558 public void handleMessage(Message msg
)
560 /* handle retry countdown */
561 if (mService
.get().mRetryTimeout
<= 0)
565 mService
.get().mRetryIn
-= RETRY_INTERVAL
;
566 if (mService
.get().mRetryIn
> 0)
568 /* calculate next interval before notifying listeners */
569 long next
= SystemClock
.uptimeMillis() + RETRY_INTERVAL
;
571 for (VpnStateListener listener
: mService
.get().mListeners
)
573 listener
.stateChanged();
575 sendMessageAtTime(obtainMessage(RETRY_MSG
), next
);
579 mService
.get().connect(null, false);
585 * Class that handles an exponential backoff for retry timeouts
587 private static class RetryTimeoutProvider
591 private long getBaseTimeout(ErrorState error
)
597 case PEER_AUTH_FAILED
:
603 case PASSWORD_MISSING
:
604 /* this needs user intervention (entering the password) */
606 case CERTIFICATE_UNAVAILABLE
:
607 /* if this is because the device has to be unlocked we might be able to reconnect */
615 * Called each time a new retry timeout is started. The timeout increases until reset() is
616 * called and the base timeout is returned again.
618 * @param error Error state
620 public long getTimeout(ErrorState error
)
622 long timeout
= (long)(getBaseTimeout(error
) * Math
.pow(2, mRetry
++));
623 /* return the result rounded to seconds */
624 return Math
.min((timeout
/ 1000) * 1000, MAX_RETRY_INTERVAL
);
628 * Reset the retry counter.