]> git.ipfire.org Git - thirdparty/strongswan.git/blame - src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java
Merge branch 'tun-device-ipv6'
[thirdparty/strongswan.git] / src / frontends / android / app / src / main / java / org / strongswan / android / logic / VpnStateService.java
CommitLineData
d1220566 1/*
59693d6c 2 * Copyright (C) 2012-2017 Tobias Brunner
19ef2aec
TB
3 *
4 * Copyright (C) secunet Security Networks AG
d1220566
TB
5 *
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>.
10 *
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
14 * for more details.
15 */
16
17package org.strongswan.android.logic;
18
d1220566 19import android.app.Service;
1b887772 20import android.content.Context;
d1220566
TB
21import android.content.Intent;
22import android.os.Binder;
063230c2 23import android.os.Bundle;
d1220566
TB
24import android.os.Handler;
25import android.os.IBinder;
dc351a30 26import android.os.Looper;
68afdd34
TB
27import android.os.Message;
28import android.os.SystemClock;
d1220566 29
a7d679ff 30import org.strongswan.android.R;
b14507dd 31import org.strongswan.android.data.VpnProfile;
063230c2 32import org.strongswan.android.data.VpnProfileDataSource;
3eda52f0 33import org.strongswan.android.data.VpnType;
b14507dd
TB
34import org.strongswan.android.logic.imc.ImcState;
35import org.strongswan.android.logic.imc.RemediationInstruction;
3eda52f0 36import org.strongswan.android.ui.VpnProfileControlActivity;
b14507dd 37
68afdd34 38import java.lang.ref.WeakReference;
b14507dd
TB
39import java.util.Collections;
40import java.util.HashSet;
41import java.util.LinkedList;
42import java.util.List;
43import java.util.concurrent.Callable;
44
3b9696fc
TB
45import androidx.core.content.ContextCompat;
46
d1220566
TB
47public class VpnStateService extends Service
48{
b14507dd 49 private final HashSet<VpnStateListener> mListeners = new HashSet<VpnStateListener>();
d1220566 50 private final IBinder mBinder = new LocalBinder();
38172313 51 private long mConnectionID = 0;
d1220566
TB
52 private Handler mHandler;
53 private VpnProfile mProfile;
54 private State mState = State.DISABLED;
55 private ErrorState mError = ErrorState.NO_ERROR;
dc52cfab 56 private ImcState mImcState = ImcState.UNKNOWN;
a05acd76 57 private final LinkedList<RemediationInstruction> mRemediationInstructions = new LinkedList<RemediationInstruction>();
8e3b921a 58 private static final long RETRY_INTERVAL = 1000;
1350ee1e 59 /* cap the retry interval at 2 minutes */
8e3b921a
MP
60 private static final long MAX_RETRY_INTERVAL = 120000;
61 private static final int RETRY_MSG = 1;
62 private final RetryTimeoutProvider mTimeoutProvider = new RetryTimeoutProvider();
68afdd34
TB
63 private long mRetryTimeout;
64 private long mRetryIn;
d1220566
TB
65
66 public enum State
67 {
68 DISABLED,
69 CONNECTING,
70 CONNECTED,
71 DISCONNECTING,
72 }
73
74 public enum ErrorState
75 {
76 NO_ERROR,
77 AUTH_FAILED,
78 PEER_AUTH_FAILED,
79 LOOKUP_FAILED,
80 UNREACHABLE,
81 GENERIC_ERROR,
f0b3e303 82 PASSWORD_MISSING,
ab5dbbc4 83 CERTIFICATE_UNAVAILABLE,
d1220566
TB
84 }
85
86 /**
87 * Listener interface for bound clients that are interested in changes to
88 * this Service.
89 */
90 public interface VpnStateListener
91 {
8e3b921a 92 void stateChanged();
d1220566
TB
93 }
94
95 /**
96 * Simple Binder that allows to directly access this Service class itself
97 * after binding to it.
98 */
99 public class LocalBinder extends Binder
100 {
101 public VpnStateService getService()
102 {
103 return VpnStateService.this;
104 }
105 }
106
107 @Override
108 public void onCreate()
109 {
110 /* this handler allows us to notify listeners from the UI thread and
111 * not from the threads that actually report any state changes */
dc351a30 112 mHandler = new RetryHandler(getMainLooper(), this);
d1220566
TB
113 }
114
115 @Override
116 public IBinder onBind(Intent intent)
117 {
118 return mBinder;
119 }
120
121 @Override
122 public void onDestroy()
123 {
124 }
125
126 /**
127 * Register a listener with this Service. We assume this is called from
128 * the main thread so no synchronization is happening.
129 *
130 * @param listener listener to register
131 */
132 public void registerListener(VpnStateListener listener)
133 {
134 mListeners.add(listener);
135 }
136
137 /**
138 * Unregister a listener from this Service.
139 *
140 * @param listener listener to unregister
141 */
142 public void unregisterListener(VpnStateListener listener)
143 {
144 mListeners.remove(listener);
145 }
146
147 /**
148 * Get the current VPN profile.
149 *
150 * @return profile
151 */
152 public VpnProfile getProfile()
153 { /* only updated from the main thread so no synchronization needed */
154 return mProfile;
155 }
156
38172313
TB
157 /**
158 * Get the current connection ID. May be used to track which state
159 * changes have already been handled.
160 *
161 * Is increased when startConnection() is called.
162 *
163 * @return connection ID
164 */
165 public long getConnectionID()
166 { /* only updated from the main thread so no synchronization needed */
167 return mConnectionID;
168 }
169
68afdd34
TB
170 /**
171 * Get the total number of seconds until there is an automatic retry to reconnect.
8e3b921a 172 *
68afdd34
TB
173 * @return total number of seconds until the retry
174 */
175 public int getRetryTimeout()
176 {
177 return (int)(mRetryTimeout / 1000);
178 }
179
180 /**
181 * Get the number of seconds until there is an automatic retry to reconnect.
8e3b921a 182 *
68afdd34
TB
183 * @return number of seconds until the retry
184 */
185 public int getRetryIn()
186 {
187 return (int)(mRetryIn / 1000);
188 }
189
d1220566
TB
190 /**
191 * Get the current state.
192 *
193 * @return state
194 */
195 public State getState()
196 { /* only updated from the main thread so no synchronization needed */
197 return mState;
198 }
199
200 /**
201 * Get the current error, if any.
202 *
203 * @return error
204 */
205 public ErrorState getErrorState()
206 { /* only updated from the main thread so no synchronization needed */
207 return mError;
208 }
209
a7d679ff
TB
210 /**
211 * Get a description of the current error, if any.
212 *
213 * @return error description text id
214 */
215 public int getErrorText()
216 {
217 switch (mError)
218 {
219 case AUTH_FAILED:
220 if (mImcState == ImcState.BLOCK)
221 {
222 return R.string.error_assessment_failed;
223 }
224 else
225 {
226 return R.string.error_auth_failed;
227 }
228 case PEER_AUTH_FAILED:
229 return R.string.error_peer_auth_failed;
230 case LOOKUP_FAILED:
231 return R.string.error_lookup_failed;
232 case UNREACHABLE:
233 return R.string.error_unreachable;
f0b3e303
TB
234 case PASSWORD_MISSING:
235 return R.string.error_password_missing;
ab5dbbc4
TB
236 case CERTIFICATE_UNAVAILABLE:
237 return R.string.error_certificate_unavailable;
a7d679ff
TB
238 default:
239 return R.string.error_generic;
240 }
241 }
242
dc52cfab
TB
243 /**
244 * Get the current IMC state, if any.
245 *
246 * @return imc state
247 */
248 public ImcState getImcState()
249 { /* only updated from the main thread so no synchronization needed */
250 return mImcState;
251 }
252
a05acd76
TB
253 /**
254 * Get the remediation instructions, if any.
255 *
256 * @return read-only list of instructions
257 */
258 public List<RemediationInstruction> getRemediationInstructions()
259 { /* only updated from the main thread so no synchronization needed */
260 return Collections.unmodifiableList(mRemediationInstructions);
261 }
262
1b887772
TB
263 /**
264 * Disconnect any existing connection and shutdown the daemon, the
265 * VpnService is not stopped but it is reset so new connections can be
266 * started.
267 */
268 public void disconnect()
269 {
3f71118b 270 /* reset any potential retry timer and error state */
68afdd34 271 resetRetryTimer();
3f71118b
TB
272 setError(ErrorState.NO_ERROR);
273
1b887772
TB
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,
59693d6c 278 * instead we call startService() with a specific action which shuts down
1b887772
TB
279 * the daemon (and closes the TUN device, if any) */
280 Context context = getApplicationContext();
281 Intent intent = new Intent(context, CharonVpnService.class);
59693d6c 282 intent.setAction(CharonVpnService.DISCONNECT_ACTION);
1b887772
TB
283 context.startService(intent);
284 }
285
1350ee1e
TB
286 /**
287 * Connect (or reconnect) a profile
8e3b921a 288 *
1350ee1e 289 * @param profileInfo optional profile info (basically the UUID and password), taken from the
8e3b921a 290 * previous profile if null
1350ee1e
TB
291 * @param fromScratch true if this is a manual retry/reconnect or a completely new connection
292 */
293 public void connect(Bundle profileInfo, boolean fromScratch)
294 {
295 /* we assume we have the necessary permission */
296 Context context = getApplicationContext();
297 Intent intent = new Intent(context, CharonVpnService.class);
298 if (profileInfo == null)
299 {
300 profileInfo = new Bundle();
6f9b96ac 301 profileInfo.putString(VpnProfileDataSource.KEY_UUID, mProfile.getUUID().toString());
1350ee1e
TB
302 /* pass the previous password along */
303 profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, mProfile.getPassword());
304 }
305 if (fromScratch)
306 {
307 /* reset if this is a manual retry or a new connection */
308 mTimeoutProvider.reset();
309 }
fb3772ec
TB
310 else
311 { /* mark this as an automatic retry */
312 profileInfo.putBoolean(CharonVpnService.KEY_IS_RETRY, true);
313 }
1350ee1e 314 intent.putExtras(profileInfo);
5bdb800a 315 ContextCompat.startForegroundService(context, intent);
1350ee1e
TB
316 }
317
063230c2
TB
318 /**
319 * Reconnect to the previous profile.
320 */
321 public void reconnect()
322 {
323 if (mProfile == null)
324 {
325 return;
326 }
3eda52f0
TB
327 if (mProfile.getVpnType().has(VpnType.VpnTypeFeature.USER_PASS))
328 {
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);
8e3b921a 336 intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_UUID, mProfile.getUUID().toString());
3eda52f0
TB
337 startActivity(intent);
338 /* reset the retry timer immediately in case the user needs more time to enter the password */
339 notifyListeners(() -> {
340 resetRetryTimer();
341 return true;
342 });
343 return;
344 }
345 }
1350ee1e 346 connect(null, true);
063230c2
TB
347 }
348
d1220566
TB
349 /**
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.
354 *
355 * @param change the state update to perform before notifying listeners, returns true if state changed
356 */
357 private void notifyListeners(final Callable<Boolean> change)
358 {
8e3b921a
MP
359 mHandler.post(new Runnable()
360 {
d1220566
TB
361 @Override
362 public void run()
363 {
364 try
365 {
366 if (change.call())
367 { /* otherwise there is no need to notify the listeners */
368 for (VpnStateListener listener : mListeners)
369 {
370 listener.stateChanged();
371 }
372 }
373 }
374 catch (Exception e)
375 {
376 e.printStackTrace();
377 }
378 }
379 });
380 }
381
382 /**
38172313
TB
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.
d1220566
TB
386 *
387 * May be called from threads other than the main thread.
388 *
389 * @param profile current profile
390 */
38172313 391 public void startConnection(final VpnProfile profile)
d1220566 392 {
8e3b921a
MP
393 notifyListeners(new Callable<Boolean>()
394 {
d1220566 395 @Override
38172313 396 public Boolean call() throws Exception
d1220566 397 {
68afdd34 398 resetRetryTimer();
38172313 399 VpnStateService.this.mConnectionID++;
d1220566 400 VpnStateService.this.mProfile = profile;
38172313
TB
401 VpnStateService.this.mState = State.CONNECTING;
402 VpnStateService.this.mError = ErrorState.NO_ERROR;
403 VpnStateService.this.mImcState = ImcState.UNKNOWN;
cfed5679 404 VpnStateService.this.mRemediationInstructions.clear();
38172313 405 return true;
d1220566
TB
406 }
407 });
408 }
409
410 /**
411 * Update the state and notify all listeners, if changed.
412 *
413 * May be called from threads other than the main thread.
414 *
415 * @param state new state
416 */
417 public void setState(final State state)
418 {
8e3b921a
MP
419 notifyListeners(new Callable<Boolean>()
420 {
d1220566
TB
421 @Override
422 public Boolean call() throws Exception
423 {
1350ee1e
TB
424 if (state == State.CONNECTED)
425 { /* reset counter in case there is an error later on */
426 mTimeoutProvider.reset();
427 }
d1220566
TB
428 if (VpnStateService.this.mState != state)
429 {
430 VpnStateService.this.mState = state;
431 return true;
432 }
433 return false;
434 }
435 });
436 }
437
438 /**
439 * Set the current error state and notify all listeners, if changed.
440 *
441 * May be called from threads other than the main thread.
442 *
443 * @param error error state
444 */
445 public void setError(final ErrorState error)
446 {
8e3b921a
MP
447 notifyListeners(new Callable<Boolean>()
448 {
d1220566
TB
449 @Override
450 public Boolean call() throws Exception
451 {
452 if (VpnStateService.this.mError != error)
453 {
68afdd34
TB
454 if (VpnStateService.this.mError == ErrorState.NO_ERROR)
455 {
456 setRetryTimer(error);
457 }
458 else if (error == ErrorState.NO_ERROR)
459 {
460 resetRetryTimer();
461 }
d1220566
TB
462 VpnStateService.this.mError = error;
463 return true;
464 }
465 return false;
466 }
467 });
468 }
dc52cfab
TB
469
470 /**
471 * Set the current IMC state and notify all listeners, if changed.
472 *
a05acd76
TB
473 * Setting the state to UNKNOWN clears all remediation instructions.
474 *
dc52cfab
TB
475 * May be called from threads other than the main thread.
476 *
d5070425 477 * @param state IMC state
dc52cfab
TB
478 */
479 public void setImcState(final ImcState state)
480 {
8e3b921a
MP
481 notifyListeners(new Callable<Boolean>()
482 {
dc52cfab
TB
483 @Override
484 public Boolean call() throws Exception
485 {
a05acd76
TB
486 if (state == ImcState.UNKNOWN)
487 {
488 VpnStateService.this.mRemediationInstructions.clear();
489 }
dc52cfab
TB
490 if (VpnStateService.this.mImcState != state)
491 {
492 VpnStateService.this.mImcState = state;
493 return true;
494 }
495 return false;
496 }
497 });
498 }
a05acd76
TB
499
500 /**
501 * Add the given remediation instruction to the internal list. Listeners
502 * are not notified.
503 *
504 * Instructions are cleared if the IMC state is set to UNKNOWN.
505 *
506 * May be called from threads other than the main thread.
507 *
508 * @param instruction remediation instruction
509 */
510 public void addRemediationInstruction(final RemediationInstruction instruction)
511 {
8e3b921a
MP
512 mHandler.post(new Runnable()
513 {
a05acd76
TB
514 @Override
515 public void run()
516 {
517 VpnStateService.this.mRemediationInstructions.add(instruction);
518 }
519 });
520 }
68afdd34
TB
521
522 /**
523 * Sets the retry timer
524 */
525 private void setRetryTimer(ErrorState error)
526 {
1350ee1e
TB
527 mRetryTimeout = mRetryIn = mTimeoutProvider.getTimeout(error);
528 if (mRetryTimeout <= 0)
68afdd34
TB
529 {
530 return;
531 }
532 mHandler.sendMessageAtTime(mHandler.obtainMessage(RETRY_MSG), SystemClock.uptimeMillis() + RETRY_INTERVAL);
533 }
534
535 /**
536 * Reset the retry timer
537 */
538 private void resetRetryTimer()
539 {
540 mRetryTimeout = 0;
541 mRetryIn = 0;
542 }
543
544 /**
545 * Special Handler subclass that handles the retry countdown (more accurate than CountDownTimer)
546 */
8e3b921a
MP
547 private static class RetryHandler extends Handler
548 {
68afdd34
TB
549 WeakReference<VpnStateService> mService;
550
dc351a30 551 public RetryHandler(Looper looper, VpnStateService service)
68afdd34 552 {
dc351a30 553 super(looper);
68afdd34
TB
554 mService = new WeakReference<>(service);
555 }
556
557 @Override
558 public void handleMessage(Message msg)
559 {
560 /* handle retry countdown */
561 if (mService.get().mRetryTimeout <= 0)
562 {
563 return;
564 }
565 mService.get().mRetryIn -= RETRY_INTERVAL;
566 if (mService.get().mRetryIn > 0)
567 {
568 /* calculate next interval before notifying listeners */
569 long next = SystemClock.uptimeMillis() + RETRY_INTERVAL;
570
571 for (VpnStateListener listener : mService.get().mListeners)
572 {
573 listener.stateChanged();
574 }
575 sendMessageAtTime(obtainMessage(RETRY_MSG), next);
576 }
577 else
578 {
1350ee1e 579 mService.get().connect(null, false);
68afdd34
TB
580 }
581 }
582 }
1350ee1e
TB
583
584 /**
585 * Class that handles an exponential backoff for retry timeouts
586 */
587 private static class RetryTimeoutProvider
588 {
589 private long mRetry;
590
591 private long getBaseTimeout(ErrorState error)
592 {
593 switch (error)
594 {
595 case AUTH_FAILED:
596 return 10000;
597 case PEER_AUTH_FAILED:
598 return 5000;
599 case LOOKUP_FAILED:
600 return 5000;
601 case UNREACHABLE:
602 return 5000;
603 case PASSWORD_MISSING:
604 /* this needs user intervention (entering the password) */
605 return 0;
606 case CERTIFICATE_UNAVAILABLE:
607 /* if this is because the device has to be unlocked we might be able to reconnect */
608 return 5000;
609 default:
610 return 10000;
611 }
612 }
613
614 /**
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.
8e3b921a 617 *
1350ee1e
TB
618 * @param error Error state
619 */
620 public long getTimeout(ErrorState error)
621 {
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);
625 }
626
627 /**
628 * Reset the retry counter.
629 */
630 public void reset()
631 {
632 mRetry = 0;
633 }
634 }
d1220566 635}