android: Add an automatic reconnect on errors

This way the connection will be attempted to be kept up even on "fatal"
errors like authentication failures.
This commit is contained in:
Tobias Brunner
2018-07-03 11:31:38 +02:00
parent 89a88d7c56
commit 68afdd3464
10 changed files with 191 additions and 24 deletions
@@ -22,6 +22,8 @@ import android.os.Binder;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.Message;
import android.os.SystemClock;
import org.strongswan.android.R; import org.strongswan.android.R;
import org.strongswan.android.data.VpnProfile; import org.strongswan.android.data.VpnProfile;
@@ -29,6 +31,7 @@ import org.strongswan.android.data.VpnProfileDataSource;
import org.strongswan.android.logic.imc.ImcState; import org.strongswan.android.logic.imc.ImcState;
import org.strongswan.android.logic.imc.RemediationInstruction; import org.strongswan.android.logic.imc.RemediationInstruction;
import java.lang.ref.WeakReference;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
@@ -46,6 +49,10 @@ public class VpnStateService extends Service
private ErrorState mError = ErrorState.NO_ERROR; private ErrorState mError = ErrorState.NO_ERROR;
private ImcState mImcState = ImcState.UNKNOWN; private ImcState mImcState = ImcState.UNKNOWN;
private final LinkedList<RemediationInstruction> mRemediationInstructions = new LinkedList<RemediationInstruction>(); private final LinkedList<RemediationInstruction> mRemediationInstructions = new LinkedList<RemediationInstruction>();
private static long RETRY_INTERVAL = 1000;
private static int RETRY_MSG = 1;
private long mRetryTimeout;
private long mRetryIn;
public enum State public enum State
{ {
@@ -93,7 +100,7 @@ public class VpnStateService extends Service
{ {
/* this handler allows us to notify listeners from the UI thread and /* this handler allows us to notify listeners from the UI thread and
* not from the threads that actually report any state changes */ * not from the threads that actually report any state changes */
mHandler = new Handler(); mHandler = new RetryHandler(this);
} }
@Override @Override
@@ -151,6 +158,24 @@ public class VpnStateService extends Service
return mConnectionID; return mConnectionID;
} }
/**
* Get the total number of seconds until there is an automatic retry to reconnect.
* @return total number of seconds until the retry
*/
public int getRetryTimeout()
{
return (int)(mRetryTimeout / 1000);
}
/**
* Get the number of seconds until there is an automatic retry to reconnect.
* @return number of seconds until the retry
*/
public int getRetryIn()
{
return (int)(mRetryIn / 1000);
}
/** /**
* Get the current state. * Get the current state.
* *
@@ -231,6 +256,7 @@ public class VpnStateService extends Service
*/ */
public void disconnect() public void disconnect()
{ {
resetRetryTimer();
/* as soon as the TUN device is created by calling establish() on the /* as soon as the TUN device is created by calling establish() on the
* VpnService.Builder object the system binds to the service and keeps * VpnService.Builder object the system binds to the service and keeps
* bound until the file descriptor of the TUN device is closed. thus * bound until the file descriptor of the TUN device is closed. thus
@@ -310,6 +336,7 @@ public class VpnStateService extends Service
@Override @Override
public Boolean call() throws Exception public Boolean call() throws Exception
{ {
resetRetryTimer();
VpnStateService.this.mConnectionID++; VpnStateService.this.mConnectionID++;
VpnStateService.this.mProfile = profile; VpnStateService.this.mProfile = profile;
VpnStateService.this.mState = State.CONNECTING; VpnStateService.this.mState = State.CONNECTING;
@@ -359,6 +386,14 @@ public class VpnStateService extends Service
{ {
if (VpnStateService.this.mError != error) if (VpnStateService.this.mError != error)
{ {
if (VpnStateService.this.mError == ErrorState.NO_ERROR)
{
setRetryTimer(error);
}
else if (error == ErrorState.NO_ERROR)
{
resetRetryTimer();
}
VpnStateService.this.mError = error; VpnStateService.this.mError = error;
return true; return true;
} }
@@ -416,4 +451,92 @@ public class VpnStateService extends Service
} }
}); });
} }
/**
* Sets the retry timer
*/
private void setRetryTimer(ErrorState error)
{
long timeout;
switch (error)
{
case AUTH_FAILED:
timeout = 20000;
break;
case PEER_AUTH_FAILED:
timeout = 20000;
break;
case LOOKUP_FAILED:
timeout = 10000;
break;
case UNREACHABLE:
timeout = 10000;
break;
case PASSWORD_MISSING:
/* this needs user intervention (entering the password) */
timeout = 0;
break;
case CERTIFICATE_UNAVAILABLE:
/* if this is because the device has to be unlocked we might be able to reconnect */
timeout = 10000;
break;
default:
timeout = 20000;
break;
}
mRetryTimeout = mRetryIn = timeout;
if (timeout <= 0)
{
return;
}
mHandler.sendMessageAtTime(mHandler.obtainMessage(RETRY_MSG), SystemClock.uptimeMillis() + RETRY_INTERVAL);
}
/**
* Reset the retry timer
*/
private void resetRetryTimer()
{
mRetryTimeout = 0;
mRetryIn = 0;
}
/**
* Special Handler subclass that handles the retry countdown (more accurate than CountDownTimer)
*/
private static class RetryHandler extends Handler {
WeakReference<VpnStateService> mService;
public RetryHandler(VpnStateService service)
{
mService = new WeakReference<>(service);
}
@Override
public void handleMessage(Message msg)
{
/* handle retry countdown */
if (mService.get().mRetryTimeout <= 0)
{
return;
}
mService.get().mRetryIn -= RETRY_INTERVAL;
if (mService.get().mRetryIn > 0)
{
/* calculate next interval before notifying listeners */
long next = SystemClock.uptimeMillis() + RETRY_INTERVAL;
for (VpnStateListener listener : mService.get().mListeners)
{
listener.stateChanged();
}
sendMessageAtTime(obtainMessage(RETRY_MSG), next);
}
else
{
mService.get().reconnect();
}
}
}
} }
@@ -41,11 +41,6 @@ import org.strongswan.android.logic.VpnStateService;
import org.strongswan.android.logic.VpnStateService.ErrorState; import org.strongswan.android.logic.VpnStateService.ErrorState;
import org.strongswan.android.logic.VpnStateService.State; import org.strongswan.android.logic.VpnStateService.State;
import org.strongswan.android.logic.VpnStateService.VpnStateListener; import org.strongswan.android.logic.VpnStateService.VpnStateListener;
import org.strongswan.android.logic.imc.ImcState;
import org.strongswan.android.logic.imc.RemediationInstruction;
import java.util.ArrayList;
import java.util.List;
public class VpnStateFragment extends Fragment implements VpnStateListener public class VpnStateFragment extends Fragment implements VpnStateListener
{ {
@@ -62,7 +57,7 @@ public class VpnStateFragment extends Fragment implements VpnStateListener
private LinearLayout mErrorView; private LinearLayout mErrorView;
private TextView mErrorText; private TextView mErrorText;
private Button mErrorRetry; private Button mErrorRetry;
private Button mDismissError; private Button mShowLog;
private long mErrorConnectionID; private long mErrorConnectionID;
private VpnStateService mService; private VpnStateService mService;
private final ServiceConnection mServiceConnection = new ServiceConnection() private final ServiceConnection mServiceConnection = new ServiceConnection()
@@ -128,12 +123,13 @@ public class VpnStateFragment extends Fragment implements VpnStateListener
View view = inflater.inflate(R.layout.vpn_state_fragment, null); View view = inflater.inflate(R.layout.vpn_state_fragment, null);
mActionButton = (Button)view.findViewById(R.id.action); mActionButton = (Button)view.findViewById(R.id.action);
mActionButton.setOnClickListener(v -> clearError());
enableActionButton(null); enableActionButton(null);
mErrorView = view.findViewById(R.id.vpn_error); mErrorView = view.findViewById(R.id.vpn_error);
mErrorText = view.findViewById(R.id.vpn_error_text); mErrorText = view.findViewById(R.id.vpn_error_text);
mErrorRetry = view.findViewById(R.id.retry); mErrorRetry = view.findViewById(R.id.retry);
mDismissError = view.findViewById(R.id.dismiss_error); mShowLog = view.findViewById(R.id.show_log);
mProgress = (ProgressBar)view.findViewById(R.id.progress); mProgress = (ProgressBar)view.findViewById(R.id.progress);
mStateView = (TextView)view.findViewById(R.id.vpn_state); mStateView = (TextView)view.findViewById(R.id.vpn_state);
mColorStateBase = mStateView.getCurrentTextColor(); mColorStateBase = mStateView.getCurrentTextColor();
@@ -146,7 +142,10 @@ public class VpnStateFragment extends Fragment implements VpnStateListener
mService.reconnect(); mService.reconnect();
} }
}); });
mDismissError.setOnClickListener(v -> clearError()); mShowLog.setOnClickListener(v -> {
Intent intent = new Intent(getActivity(), LogActivity.class);
startActivity(intent);
});
return view; return view;
} }
@@ -194,7 +193,6 @@ public class VpnStateFragment extends Fragment implements VpnStateListener
VpnProfile profile = mService.getProfile(); VpnProfile profile = mService.getProfile();
State state = mService.getState(); State state = mService.getState();
ErrorState error = mService.getErrorState(); ErrorState error = mService.getErrorState();
ImcState imcState = mService.getImcState();
String name = ""; String name = "";
if (getActivity() == null) if (getActivity() == null)
@@ -207,12 +205,13 @@ public class VpnStateFragment extends Fragment implements VpnStateListener
name = profile.getName(); name = profile.getName();
} }
if (reportError(connectionID, name, error, imcState)) if (reportError(connectionID, name, error))
{ {
return; return;
} }
mProfileNameView.setText(name); mProfileNameView.setText(name);
mProgress.setIndeterminate(true);
switch (state) switch (state)
{ {
@@ -247,7 +246,7 @@ public class VpnStateFragment extends Fragment implements VpnStateListener
} }
} }
private boolean reportError(long connectionID, String name, ErrorState error, ImcState imcState) private boolean reportError(long connectionID, String name, ErrorState error)
{ {
if (error == ErrorState.NO_ERROR) if (error == ErrorState.NO_ERROR)
{ {
@@ -257,15 +256,26 @@ public class VpnStateFragment extends Fragment implements VpnStateListener
mErrorConnectionID = connectionID; mErrorConnectionID = connectionID;
mProfileNameView.setText(name); mProfileNameView.setText(name);
showProfile(true); showProfile(true);
mProgress.setVisibility(View.GONE);
mStateView.setText(R.string.state_error); mStateView.setText(R.string.state_error);
mStateView.setTextColor(mColorStateError); mStateView.setTextColor(mColorStateError);
enableActionButton(getString(R.string.show_log)); enableActionButton(getString(android.R.string.cancel));
mActionButton.setOnClickListener(v -> {
Intent intent = new Intent(getActivity(), LogActivity.class); int retry = mService.getRetryIn();
startActivity(intent); if (retry > 0)
}); {
mErrorText.setText(getString(R.string.error_format, getString(mService.getErrorText()))); mProgress.setIndeterminate(false);
mProgress.setMax(mService.getRetryTimeout());
mProgress.setProgress(retry);
mProgress.setVisibility(View.VISIBLE);
mStateView.setText(getResources().getQuantityString(R.plurals.retry_in, retry, retry));
}
else if (mService.getRetryTimeout() <= 0)
{
mProgress.setVisibility(View.GONE);
}
String text = getString(R.string.error_format, getString(mService.getErrorText()));
mErrorText.setText(text);
mErrorView.setVisibility(View.VISIBLE); mErrorView.setVisibility(View.VISIBLE);
return true; return true;
} }
@@ -281,19 +291,17 @@ public class VpnStateFragment extends Fragment implements VpnStateListener
mActionButton.setText(text); mActionButton.setText(text);
mActionButton.setEnabled(text != null); mActionButton.setEnabled(text != null);
mActionButton.setVisibility(text != null ? View.VISIBLE : View.GONE); mActionButton.setVisibility(text != null ? View.VISIBLE : View.GONE);
mActionButton.setOnClickListener(mDisconnectListener);
} }
private void clearError() private void clearError()
{ {
if (mService != null) if (mService != null)
{ {
mService.disconnect();
if (mService.getConnectionID() == mErrorConnectionID) if (mService.getConnectionID() == mErrorConnectionID)
{ {
mService.disconnect();
mService.setError(ErrorState.NO_ERROR); mService.setError(ErrorState.NO_ERROR);
} }
} }
updateView();
} }
} }
@@ -34,6 +34,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="20dp" android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
android:text="Failed to establish VPN: Server is unreachable" android:text="Failed to establish VPN: Server is unreachable"
@@ -48,11 +49,11 @@
android:gravity="end" > android:gravity="end" >
<Button <Button
android:id="@+id/dismiss_error" android:id="@+id/show_log"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:text="@string/dismiss" android:text="@string/show_log"
android:textColor="@color/primary" android:textColor="@color/primary"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="bold" android:textStyle="bold"
@@ -198,6 +198,11 @@
<string name="disconnect_active_connection">Dies trennt die aktuelle VPN Verbindung!</string> <string name="disconnect_active_connection">Dies trennt die aktuelle VPN Verbindung!</string>
<string name="connect">Verbinden</string> <string name="connect">Verbinden</string>
<string name="retry">Wiederholen</string> <string name="retry">Wiederholen</string>
<plurals name="retry_in">
<item quantity="one">Wiederholen in %1$d Sekunde</item>
<item quantity="other">Wiederholen in %1$d Sekunden</item>
</plurals>
<string name="cancel_retry">Wiederholen abbrechen</string>
<!-- Quick Settings tile --> <!-- Quick Settings tile -->
<string name="tile_default">VPN umschalten</string> <string name="tile_default">VPN umschalten</string>
@@ -198,6 +198,11 @@
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string> <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
<string name="connect">Połącz</string> <string name="connect">Połącz</string>
<string name="retry">Retry</string> <string name="retry">Retry</string>
<plurals name="retry_in">
<item quantity="one">Retry in %1$d second</item>
<item quantity="other">Retry in %1$d seconds</item>
</plurals>
<string name="cancel_retry">Cancel retry</string>
<!-- Quick Settings tile --> <!-- Quick Settings tile -->
<string name="tile_default">Toggle VPN</string> <string name="tile_default">Toggle VPN</string>
@@ -195,6 +195,11 @@
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string> <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
<string name="connect">Соединить</string> <string name="connect">Соединить</string>
<string name="retry">Retry</string> <string name="retry">Retry</string>
<plurals name="retry_in">
<item quantity="one">Retry in %1$d second</item>
<item quantity="other">Retry in %1$d seconds</item>
</plurals>
<string name="cancel_retry">Cancel retry</string>
<!-- Quick Settings tile --> <!-- Quick Settings tile -->
<string name="tile_default">Toggle VPN</string> <string name="tile_default">Toggle VPN</string>
@@ -196,6 +196,11 @@
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string> <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
<string name="connect">Підключити</string> <string name="connect">Підключити</string>
<string name="retry">Retry</string> <string name="retry">Retry</string>
<plurals name="retry_in">
<item quantity="one">Retry in %1$d second</item>
<item quantity="other">Retry in %1$d seconds</item>
</plurals>
<string name="cancel_retry">Cancel retry</string>
<!-- Quick Settings tile --> <!-- Quick Settings tile -->
<string name="tile_default">Toggle VPN</string> <string name="tile_default">Toggle VPN</string>
@@ -195,6 +195,11 @@
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string> <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
<string name="connect">连接</string> <string name="connect">连接</string>
<string name="retry">Retry</string> <string name="retry">Retry</string>
<plurals name="retry_in">
<item quantity="one">Retry in %1$d second</item>
<item quantity="other">Retry in %1$d seconds</item>
</plurals>
<string name="cancel_retry">Cancel retry</string>
<!-- Quick Settings tile --> <!-- Quick Settings tile -->
<string name="tile_default">Toggle VPN</string> <string name="tile_default">Toggle VPN</string>
@@ -195,6 +195,11 @@
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string> <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
<string name="connect">連線</string> <string name="connect">連線</string>
<string name="retry">Retry</string> <string name="retry">Retry</string>
<plurals name="retry_in">
<item quantity="one">Retry in %1$d second</item>
<item quantity="other">Retry in %1$d seconds</item>
</plurals>
<string name="cancel_retry">Cancel retry</string>
<!-- Quick Settings tile --> <!-- Quick Settings tile -->
<string name="tile_default">Toggle VPN</string> <string name="tile_default">Toggle VPN</string>
@@ -198,6 +198,11 @@
<string name="disconnect_active_connection">This will disconnect the active VPN connection!</string> <string name="disconnect_active_connection">This will disconnect the active VPN connection!</string>
<string name="connect">Connect</string> <string name="connect">Connect</string>
<string name="retry">Retry</string> <string name="retry">Retry</string>
<plurals name="retry_in">
<item quantity="one">Retry in %1$d second</item>
<item quantity="other">Retry in %1$d seconds</item>
</plurals>
<string name="cancel_retry">Cancel retry</string>
<!-- Quick Settings tile --> <!-- Quick Settings tile -->
<string name="tile_default">Toggle VPN</string> <string name="tile_default">Toggle VPN</string>