]> git.ipfire.org Git - thirdparty/strongswan.git/blame - src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java
android: Add an automatic reconnect on errors
[thirdparty/strongswan.git] / src / frontends / android / app / src / main / java / org / strongswan / android / logic / VpnStateService.java
CommitLineData
d1220566 1/*
59693d6c
TB
2 * Copyright (C) 2012-2017 Tobias Brunner
3 * HSR Hochschule fuer Technik Rapperswil
d1220566
TB
4 *
5 * This program is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License as published by the
7 * Free Software Foundation; either version 2 of the License, or (at your
8 * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
9 *
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13 * for more details.
14 */
15
16package org.strongswan.android.logic;
17
d1220566 18import android.app.Service;
1b887772 19import android.content.Context;
d1220566
TB
20import android.content.Intent;
21import android.os.Binder;
063230c2 22import android.os.Bundle;
d1220566
TB
23import android.os.Handler;
24import android.os.IBinder;
68afdd34
TB
25import android.os.Message;
26import android.os.SystemClock;
d1220566 27
a7d679ff 28import org.strongswan.android.R;
b14507dd 29import org.strongswan.android.data.VpnProfile;
063230c2 30import org.strongswan.android.data.VpnProfileDataSource;
b14507dd
TB
31import org.strongswan.android.logic.imc.ImcState;
32import org.strongswan.android.logic.imc.RemediationInstruction;
33
68afdd34 34import java.lang.ref.WeakReference;
b14507dd
TB
35import java.util.Collections;
36import java.util.HashSet;
37import java.util.LinkedList;
38import java.util.List;
39import java.util.concurrent.Callable;
40
d1220566
TB
41public class VpnStateService extends Service
42{
b14507dd 43 private final HashSet<VpnStateListener> mListeners = new HashSet<VpnStateListener>();
d1220566 44 private final IBinder mBinder = new LocalBinder();
38172313 45 private long mConnectionID = 0;
d1220566
TB
46 private Handler mHandler;
47 private VpnProfile mProfile;
48 private State mState = State.DISABLED;
49 private ErrorState mError = ErrorState.NO_ERROR;
dc52cfab 50 private ImcState mImcState = ImcState.UNKNOWN;
a05acd76 51 private final LinkedList<RemediationInstruction> mRemediationInstructions = new LinkedList<RemediationInstruction>();
68afdd34
TB
52 private static long RETRY_INTERVAL = 1000;
53 private static int RETRY_MSG = 1;
54 private long mRetryTimeout;
55 private long mRetryIn;
d1220566
TB
56
57 public enum State
58 {
59 DISABLED,
60 CONNECTING,
61 CONNECTED,
62 DISCONNECTING,
63 }
64
65 public enum ErrorState
66 {
67 NO_ERROR,
68 AUTH_FAILED,
69 PEER_AUTH_FAILED,
70 LOOKUP_FAILED,
71 UNREACHABLE,
72 GENERIC_ERROR,
f0b3e303 73 PASSWORD_MISSING,
ab5dbbc4 74 CERTIFICATE_UNAVAILABLE,
d1220566
TB
75 }
76
77 /**
78 * Listener interface for bound clients that are interested in changes to
79 * this Service.
80 */
81 public interface VpnStateListener
82 {
83 public void stateChanged();
84 }
85
86 /**
87 * Simple Binder that allows to directly access this Service class itself
88 * after binding to it.
89 */
90 public class LocalBinder extends Binder
91 {
92 public VpnStateService getService()
93 {
94 return VpnStateService.this;
95 }
96 }
97
98 @Override
99 public void onCreate()
100 {
101 /* this handler allows us to notify listeners from the UI thread and
102 * not from the threads that actually report any state changes */
68afdd34 103 mHandler = new RetryHandler(this);
d1220566
TB
104 }
105
106 @Override
107 public IBinder onBind(Intent intent)
108 {
109 return mBinder;
110 }
111
112 @Override
113 public void onDestroy()
114 {
115 }
116
117 /**
118 * Register a listener with this Service. We assume this is called from
119 * the main thread so no synchronization is happening.
120 *
121 * @param listener listener to register
122 */
123 public void registerListener(VpnStateListener listener)
124 {
125 mListeners.add(listener);
126 }
127
128 /**
129 * Unregister a listener from this Service.
130 *
131 * @param listener listener to unregister
132 */
133 public void unregisterListener(VpnStateListener listener)
134 {
135 mListeners.remove(listener);
136 }
137
138 /**
139 * Get the current VPN profile.
140 *
141 * @return profile
142 */
143 public VpnProfile getProfile()
144 { /* only updated from the main thread so no synchronization needed */
145 return mProfile;
146 }
147
38172313
TB
148 /**
149 * Get the current connection ID. May be used to track which state
150 * changes have already been handled.
151 *
152 * Is increased when startConnection() is called.
153 *
154 * @return connection ID
155 */
156 public long getConnectionID()
157 { /* only updated from the main thread so no synchronization needed */
158 return mConnectionID;
159 }
160
68afdd34
TB
161 /**
162 * Get the total number of seconds until there is an automatic retry to reconnect.
163 * @return total number of seconds until the retry
164 */
165 public int getRetryTimeout()
166 {
167 return (int)(mRetryTimeout / 1000);
168 }
169
170 /**
171 * Get the number of seconds until there is an automatic retry to reconnect.
172 * @return number of seconds until the retry
173 */
174 public int getRetryIn()
175 {
176 return (int)(mRetryIn / 1000);
177 }
178
d1220566
TB
179 /**
180 * Get the current state.
181 *
182 * @return state
183 */
184 public State getState()
185 { /* only updated from the main thread so no synchronization needed */
186 return mState;
187 }
188
189 /**
190 * Get the current error, if any.
191 *
192 * @return error
193 */
194 public ErrorState getErrorState()
195 { /* only updated from the main thread so no synchronization needed */
196 return mError;
197 }
198
a7d679ff
TB
199 /**
200 * Get a description of the current error, if any.
201 *
202 * @return error description text id
203 */
204 public int getErrorText()
205 {
206 switch (mError)
207 {
208 case AUTH_FAILED:
209 if (mImcState == ImcState.BLOCK)
210 {
211 return R.string.error_assessment_failed;
212 }
213 else
214 {
215 return R.string.error_auth_failed;
216 }
217 case PEER_AUTH_FAILED:
218 return R.string.error_peer_auth_failed;
219 case LOOKUP_FAILED:
220 return R.string.error_lookup_failed;
221 case UNREACHABLE:
222 return R.string.error_unreachable;
f0b3e303
TB
223 case PASSWORD_MISSING:
224 return R.string.error_password_missing;
ab5dbbc4
TB
225 case CERTIFICATE_UNAVAILABLE:
226 return R.string.error_certificate_unavailable;
a7d679ff
TB
227 default:
228 return R.string.error_generic;
229 }
230 }
231
dc52cfab
TB
232 /**
233 * Get the current IMC state, if any.
234 *
235 * @return imc state
236 */
237 public ImcState getImcState()
238 { /* only updated from the main thread so no synchronization needed */
239 return mImcState;
240 }
241
a05acd76
TB
242 /**
243 * Get the remediation instructions, if any.
244 *
245 * @return read-only list of instructions
246 */
247 public List<RemediationInstruction> getRemediationInstructions()
248 { /* only updated from the main thread so no synchronization needed */
249 return Collections.unmodifiableList(mRemediationInstructions);
250 }
251
1b887772
TB
252 /**
253 * Disconnect any existing connection and shutdown the daemon, the
254 * VpnService is not stopped but it is reset so new connections can be
255 * started.
256 */
257 public void disconnect()
258 {
68afdd34 259 resetRetryTimer();
1b887772
TB
260 /* as soon as the TUN device is created by calling establish() on the
261 * VpnService.Builder object the system binds to the service and keeps
262 * bound until the file descriptor of the TUN device is closed. thus
263 * calling stopService() here would not stop (destroy) the service yet,
59693d6c 264 * instead we call startService() with a specific action which shuts down
1b887772
TB
265 * the daemon (and closes the TUN device, if any) */
266 Context context = getApplicationContext();
267 Intent intent = new Intent(context, CharonVpnService.class);
59693d6c 268 intent.setAction(CharonVpnService.DISCONNECT_ACTION);
1b887772
TB
269 context.startService(intent);
270 }
271
063230c2
TB
272 /**
273 * Reconnect to the previous profile.
274 */
275 public void reconnect()
276 {
277 if (mProfile == null)
278 {
279 return;
280 }
281 Bundle profileInfo = new Bundle();
282 profileInfo.putLong(VpnProfileDataSource.KEY_ID, mProfile.getId());
283 /* pass the previous password along */
284 profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, mProfile.getPassword());
285 /* we assume we have the necessary permission */
286 Context context = getApplicationContext();
287 Intent intent = new Intent(context, CharonVpnService.class);
288 intent.putExtras(profileInfo);
289 context.startService(intent);
290 }
291
d1220566
TB
292 /**
293 * Update state and notify all listeners about the change. By using a Handler
294 * this is done from the main UI thread and not the initial reporter thread.
295 * Also, in doing the actual state change from the main thread, listeners
296 * see all changes and none are skipped.
297 *
298 * @param change the state update to perform before notifying listeners, returns true if state changed
299 */
300 private void notifyListeners(final Callable<Boolean> change)
301 {
302 mHandler.post(new Runnable() {
303 @Override
304 public void run()
305 {
306 try
307 {
308 if (change.call())
309 { /* otherwise there is no need to notify the listeners */
310 for (VpnStateListener listener : mListeners)
311 {
312 listener.stateChanged();
313 }
314 }
315 }
316 catch (Exception e)
317 {
318 e.printStackTrace();
319 }
320 }
321 });
322 }
323
324 /**
38172313
TB
325 * Called when a connection is started. Sets the currently active VPN
326 * profile, resets IMC and Error state variables, sets the State to
327 * CONNECTING, increases the connection ID, and notifies all listeners.
d1220566
TB
328 *
329 * May be called from threads other than the main thread.
330 *
331 * @param profile current profile
332 */
38172313 333 public void startConnection(final VpnProfile profile)
d1220566 334 {
38172313 335 notifyListeners(new Callable<Boolean>() {
d1220566 336 @Override
38172313 337 public Boolean call() throws Exception
d1220566 338 {
68afdd34 339 resetRetryTimer();
38172313 340 VpnStateService.this.mConnectionID++;
d1220566 341 VpnStateService.this.mProfile = profile;
38172313
TB
342 VpnStateService.this.mState = State.CONNECTING;
343 VpnStateService.this.mError = ErrorState.NO_ERROR;
344 VpnStateService.this.mImcState = ImcState.UNKNOWN;
cfed5679 345 VpnStateService.this.mRemediationInstructions.clear();
38172313 346 return true;
d1220566
TB
347 }
348 });
349 }
350
351 /**
352 * Update the state and notify all listeners, if changed.
353 *
354 * May be called from threads other than the main thread.
355 *
356 * @param state new state
357 */
358 public void setState(final State state)
359 {
360 notifyListeners(new Callable<Boolean>() {
361 @Override
362 public Boolean call() throws Exception
363 {
364 if (VpnStateService.this.mState != state)
365 {
366 VpnStateService.this.mState = state;
367 return true;
368 }
369 return false;
370 }
371 });
372 }
373
374 /**
375 * Set the current error state and notify all listeners, if changed.
376 *
377 * May be called from threads other than the main thread.
378 *
379 * @param error error state
380 */
381 public void setError(final ErrorState error)
382 {
383 notifyListeners(new Callable<Boolean>() {
384 @Override
385 public Boolean call() throws Exception
386 {
387 if (VpnStateService.this.mError != error)
388 {
68afdd34
TB
389 if (VpnStateService.this.mError == ErrorState.NO_ERROR)
390 {
391 setRetryTimer(error);
392 }
393 else if (error == ErrorState.NO_ERROR)
394 {
395 resetRetryTimer();
396 }
d1220566
TB
397 VpnStateService.this.mError = error;
398 return true;
399 }
400 return false;
401 }
402 });
403 }
dc52cfab
TB
404
405 /**
406 * Set the current IMC state and notify all listeners, if changed.
407 *
a05acd76
TB
408 * Setting the state to UNKNOWN clears all remediation instructions.
409 *
dc52cfab
TB
410 * May be called from threads other than the main thread.
411 *
d5070425 412 * @param state IMC state
dc52cfab
TB
413 */
414 public void setImcState(final ImcState state)
415 {
416 notifyListeners(new Callable<Boolean>() {
417 @Override
418 public Boolean call() throws Exception
419 {
a05acd76
TB
420 if (state == ImcState.UNKNOWN)
421 {
422 VpnStateService.this.mRemediationInstructions.clear();
423 }
dc52cfab
TB
424 if (VpnStateService.this.mImcState != state)
425 {
426 VpnStateService.this.mImcState = state;
427 return true;
428 }
429 return false;
430 }
431 });
432 }
a05acd76
TB
433
434 /**
435 * Add the given remediation instruction to the internal list. Listeners
436 * are not notified.
437 *
438 * Instructions are cleared if the IMC state is set to UNKNOWN.
439 *
440 * May be called from threads other than the main thread.
441 *
442 * @param instruction remediation instruction
443 */
444 public void addRemediationInstruction(final RemediationInstruction instruction)
445 {
446 mHandler.post(new Runnable() {
447 @Override
448 public void run()
449 {
450 VpnStateService.this.mRemediationInstructions.add(instruction);
451 }
452 });
453 }
68afdd34
TB
454
455 /**
456 * Sets the retry timer
457 */
458 private void setRetryTimer(ErrorState error)
459 {
460 long timeout;
461
462 switch (error)
463 {
464 case AUTH_FAILED:
465 timeout = 20000;
466 break;
467 case PEER_AUTH_FAILED:
468 timeout = 20000;
469 break;
470 case LOOKUP_FAILED:
471 timeout = 10000;
472 break;
473 case UNREACHABLE:
474 timeout = 10000;
475 break;
476 case PASSWORD_MISSING:
477 /* this needs user intervention (entering the password) */
478 timeout = 0;
479 break;
480 case CERTIFICATE_UNAVAILABLE:
481 /* if this is because the device has to be unlocked we might be able to reconnect */
482 timeout = 10000;
483 break;
484 default:
485 timeout = 20000;
486 break;
487 }
488 mRetryTimeout = mRetryIn = timeout;
489 if (timeout <= 0)
490 {
491 return;
492 }
493 mHandler.sendMessageAtTime(mHandler.obtainMessage(RETRY_MSG), SystemClock.uptimeMillis() + RETRY_INTERVAL);
494 }
495
496 /**
497 * Reset the retry timer
498 */
499 private void resetRetryTimer()
500 {
501 mRetryTimeout = 0;
502 mRetryIn = 0;
503 }
504
505 /**
506 * Special Handler subclass that handles the retry countdown (more accurate than CountDownTimer)
507 */
508 private static class RetryHandler extends Handler {
509 WeakReference<VpnStateService> mService;
510
511 public RetryHandler(VpnStateService service)
512 {
513 mService = new WeakReference<>(service);
514 }
515
516 @Override
517 public void handleMessage(Message msg)
518 {
519 /* handle retry countdown */
520 if (mService.get().mRetryTimeout <= 0)
521 {
522 return;
523 }
524 mService.get().mRetryIn -= RETRY_INTERVAL;
525 if (mService.get().mRetryIn > 0)
526 {
527 /* calculate next interval before notifying listeners */
528 long next = SystemClock.uptimeMillis() + RETRY_INTERVAL;
529
530 for (VpnStateListener listener : mService.get().mListeners)
531 {
532 listener.stateChanged();
533 }
534 sendMessageAtTime(obtainMessage(RETRY_MSG), next);
535 }
536 else
537 {
538 mService.get().reconnect();
539 }
540 }
541 }
d1220566 542}