android: Use capped exponential backoff for automatic retries

This commit is contained in:
Tobias Brunner 2018-06-18 19:04:03 +02:00
parent 2ec6ad71d3
commit 1350ee1ec7
2 changed files with 93 additions and 44 deletions

View File

@ -50,7 +50,10 @@ public class VpnStateService extends Service
private ImcState mImcState = ImcState.UNKNOWN;
private final LinkedList<RemediationInstruction> mRemediationInstructions = new LinkedList<RemediationInstruction>();
private static long RETRY_INTERVAL = 1000;
/* cap the retry interval at 2 minutes */
private static long MAX_RETRY_INTERVAL = 120000;
private static int RETRY_MSG = 1;
private RetryTimeoutProvider mTimeoutProvider = new RetryTimeoutProvider();
private long mRetryTimeout;
private long mRetryIn;
@ -269,6 +272,33 @@ public class VpnStateService extends Service
context.startService(intent);
}
/**
* Connect (or reconnect) a profile
* @param profileInfo optional profile info (basically the UUID and password), taken from the
* previous profile if null
* @param fromScratch true if this is a manual retry/reconnect or a completely new connection
*/
public void connect(Bundle profileInfo, boolean fromScratch)
{
/* we assume we have the necessary permission */
Context context = getApplicationContext();
Intent intent = new Intent(context, CharonVpnService.class);
if (profileInfo == null)
{
profileInfo = new Bundle();
profileInfo.putLong(VpnProfileDataSource.KEY_ID, mProfile.getId());
/* pass the previous password along */
profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, mProfile.getPassword());
}
if (fromScratch)
{
/* reset if this is a manual retry or a new connection */
mTimeoutProvider.reset();
}
intent.putExtras(profileInfo);
context.startService(intent);
}
/**
* Reconnect to the previous profile.
*/
@ -278,15 +308,7 @@ public class VpnStateService extends Service
{
return;
}
Bundle profileInfo = new Bundle();
profileInfo.putLong(VpnProfileDataSource.KEY_ID, mProfile.getId());
/* pass the previous password along */
profileInfo.putString(VpnProfileDataSource.KEY_PASSWORD, mProfile.getPassword());
/* we assume we have the necessary permission */
Context context = getApplicationContext();
Intent intent = new Intent(context, CharonVpnService.class);
intent.putExtras(profileInfo);
context.startService(intent);
connect(null, true);
}
/**
@ -361,6 +383,10 @@ public class VpnStateService extends Service
@Override
public Boolean call() throws Exception
{
if (state == State.CONNECTED)
{ /* reset counter in case there is an error later on */
mTimeoutProvider.reset();
}
if (VpnStateService.this.mState != state)
{
VpnStateService.this.mState = state;
@ -457,36 +483,8 @@ public class VpnStateService extends Service
*/
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)
mRetryTimeout = mRetryIn = mTimeoutProvider.getTimeout(error);
if (mRetryTimeout <= 0)
{
return;
}
@ -535,8 +533,59 @@ public class VpnStateService extends Service
}
else
{
mService.get().reconnect();
mService.get().connect(null, false);
}
}
}
/**
* Class that handles an exponential backoff for retry timeouts
*/
private static class RetryTimeoutProvider
{
private long mRetry;
private long getBaseTimeout(ErrorState error)
{
switch (error)
{
case AUTH_FAILED:
return 10000;
case PEER_AUTH_FAILED:
return 5000;
case LOOKUP_FAILED:
return 5000;
case UNREACHABLE:
return 5000;
case PASSWORD_MISSING:
/* this needs user intervention (entering the password) */
return 0;
case CERTIFICATE_UNAVAILABLE:
/* if this is because the device has to be unlocked we might be able to reconnect */
return 5000;
default:
return 10000;
}
}
/**
* Called each time a new retry timeout is started. The timeout increases until reset() is
* called and the base timeout is returned again.
* @param error Error state
*/
public long getTimeout(ErrorState error)
{
long timeout = (long)(getBaseTimeout(error) * Math.pow(2, mRetry++));
/* return the result rounded to seconds */
return Math.min((timeout / 1000) * 1000, MAX_RETRY_INTERVAL);
}
/**
* Reset the retry counter.
*/
public void reset()
{
mRetry = 0;
}
}
}

View File

@ -40,7 +40,6 @@ import org.strongswan.android.R;
import org.strongswan.android.data.VpnProfile;
import org.strongswan.android.data.VpnProfileDataSource;
import org.strongswan.android.data.VpnType.VpnTypeFeature;
import org.strongswan.android.logic.CharonVpnService;
import org.strongswan.android.logic.VpnStateService;
import org.strongswan.android.logic.VpnStateService.State;
@ -176,9 +175,10 @@ public class VpnProfileControlActivity extends AppCompatActivity
case PREPARE_VPN_SERVICE:
if (resultCode == RESULT_OK && mProfileInfo != null)
{
Intent intent = new Intent(this, CharonVpnService.class);
intent.putExtras(mProfileInfo);
this.startService(intent);
if (mService != null)
{
mService.connect(mProfileInfo, true);
}
finish();
}
else