]>
Commit | Line | Data |
---|---|---|
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 | ||
16 | package org.strongswan.android.logic; | |
17 | ||
d1220566 | 18 | import android.app.Service; |
1b887772 | 19 | import android.content.Context; |
d1220566 TB |
20 | import android.content.Intent; |
21 | import android.os.Binder; | |
063230c2 | 22 | import android.os.Bundle; |
d1220566 TB |
23 | import android.os.Handler; |
24 | import android.os.IBinder; | |
68afdd34 TB |
25 | import android.os.Message; |
26 | import android.os.SystemClock; | |
d1220566 | 27 | |
a7d679ff | 28 | import org.strongswan.android.R; |
b14507dd | 29 | import org.strongswan.android.data.VpnProfile; |
063230c2 | 30 | import org.strongswan.android.data.VpnProfileDataSource; |
b14507dd TB |
31 | import org.strongswan.android.logic.imc.ImcState; |
32 | import org.strongswan.android.logic.imc.RemediationInstruction; | |
33 | ||
68afdd34 | 34 | import java.lang.ref.WeakReference; |
b14507dd TB |
35 | import java.util.Collections; |
36 | import java.util.HashSet; | |
37 | import java.util.LinkedList; | |
38 | import java.util.List; | |
39 | import java.util.concurrent.Callable; | |
40 | ||
d1220566 TB |
41 | public 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 | } |