android: resilience of F_Profile at work: backup ports, reserve server, bridge while whitelists are on
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e7b801d584
commit
5ca60d7066
6 files changed
+230
No files matched your search
+6
@@ -270,6 +270,12 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
|
||||
if (profile != null)
|
||||
{
|
||||
profile.setPassword(bundle.getString(VpnProfileDataSource.KEY_PASSWORD));
|
||||
/* a backup port of F_Profile, for this attempt only */
|
||||
int port = bundle.getInt(org.strongswan.android.logic.profiles.Resilience.KEY_PORT_OVERRIDE, 0);
|
||||
if (port > 0)
|
||||
{
|
||||
profile.setPort(port);
|
||||
}
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||
.putString(Constants.PREF_MRU_VPN_PROFILE, profile.getUUID().toString())
|
||||
.apply();
|
||||
|
||||
+15
@@ -35,6 +35,7 @@ import org.strongswan.android.ui.widget.ShxWidgetState;
|
||||
import org.strongswan.android.data.VpnProfileDataSource;
|
||||
import org.strongswan.android.data.VpnType;
|
||||
import org.strongswan.android.logic.events.Events;
|
||||
import org.strongswan.android.logic.profiles.Resilience;
|
||||
import org.strongswan.android.logic.imc.ImcState;
|
||||
import org.strongswan.android.logic.imc.RemediationInstruction;
|
||||
import org.strongswan.android.logic.connectivity.WhiteListManager;
|
||||
@@ -448,6 +449,10 @@ public class VpnStateService extends Service
|
||||
{
|
||||
uuid = mProfile.getUUID().toString();
|
||||
}
|
||||
if (fromScratch)
|
||||
{ /* a user's connect starts a new resilience attempt */
|
||||
Resilience.onConnect(uuid, profileInfo);
|
||||
}
|
||||
/* double-start guard: while a healthy connect attempt for the same
|
||||
* profile is in flight (or was just issued and the service has not
|
||||
* reached startConnection() yet) a second start intent only churns
|
||||
@@ -456,6 +461,7 @@ public class VpnStateService extends Service
|
||||
* here within 20 seconds. Deliberate retries during an error backoff
|
||||
* (mError set) and requests for another profile always pass. */
|
||||
if (uuid != null && uuid.equals(mLastConnectUuid) && mError == ErrorState.NO_ERROR
|
||||
&& !profileInfo.getBoolean(Resilience.KEY_INTERNAL, false)
|
||||
&& (mState == State.CONNECTING
|
||||
|| ((mState == State.DISABLED || mState == State.DISCONNECTING)
|
||||
&& SystemClock.uptimeMillis() - mLastConnectIssued < CONNECT_DEDUPE_MS)))
|
||||
@@ -692,6 +698,7 @@ public class VpnStateService extends Service
|
||||
if (state == State.CONNECTED)
|
||||
{
|
||||
Events.connectionRestored(VpnStateService.this);
|
||||
Resilience.onConnected(VpnStateService.this);
|
||||
}
|
||||
if (state == State.CONNECTING)
|
||||
{ /* reentered while the daemon reestablishes the tunnel; bound
|
||||
@@ -725,6 +732,14 @@ public class VpnStateService extends Service
|
||||
{
|
||||
if (VpnStateService.this.mError != error)
|
||||
{
|
||||
/* the server does not answer: backup ports, then the
|
||||
* reserve server, before the regular backoff */
|
||||
if (VpnStateService.this.mError == ErrorState.NO_ERROR
|
||||
&& (error == ErrorState.UNREACHABLE || error == ErrorState.LOOKUP_FAILED)
|
||||
&& Resilience.onUnreachable(VpnStateService.this))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (VpnStateService.this.mError == ErrorState.NO_ERROR)
|
||||
{
|
||||
setRetryTimer(error);
|
||||
|
||||
+2
@@ -442,10 +442,12 @@ class WhiteListManager private constructor(private val context: Context) : VpnSt
|
||||
if (before.status != WhiteListStatus.ACTIVE && machine.status == WhiteListStatus.ACTIVE)
|
||||
{
|
||||
Events.whitelist(context, true)
|
||||
android.os.Handler(android.os.Looper.getMainLooper()).post { org.strongswan.android.logic.profiles.Resilience.onWhitelist(context, true) }
|
||||
}
|
||||
else if (before.status == WhiteListStatus.ACTIVE && machine.status == WhiteListStatus.INACTIVE)
|
||||
{
|
||||
Events.whitelist(context, false)
|
||||
android.os.Handler(android.os.Looper.getMainLooper()).post { org.strongswan.android.logic.profiles.Resilience.onWhitelist(context, false) }
|
||||
}
|
||||
if (machine.status == WhiteListStatus.ACTIVE)
|
||||
{ /* keep watching for the whitelist to clear, at a low rate */
|
||||
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright (C) 2026 SHX VPN
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
package org.strongswan.android.logic.profiles
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import org.strongswan.android.R
|
||||
import org.strongswan.android.data.VpnProfileDataSource
|
||||
import org.strongswan.android.data.VpnProfileSource
|
||||
import org.strongswan.android.data.VpnType
|
||||
import org.strongswan.android.logic.DiagLog
|
||||
import org.strongswan.android.logic.VpnStateService
|
||||
import org.strongswan.android.logic.events.EventCategory
|
||||
import org.strongswan.android.logic.events.EventKind
|
||||
import org.strongswan.android.logic.events.EventLog
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* The resilience settings of F_Profile at work. When the server does not
|
||||
* answer, the backup ports are tried in turn, then the reserve server; only
|
||||
* then the regular retry backoff takes over. While the operator whitelists
|
||||
* are on, a profile with «Bridge for whitelists» switches to its bridge and
|
||||
* comes back when they clear.
|
||||
*
|
||||
* Called on the main thread by [VpnStateService] and the whitelist detector.
|
||||
*/
|
||||
object Resilience
|
||||
{
|
||||
private const val TAG = "Resilience"
|
||||
/** marks the connects started here (they keep the attempt state) */
|
||||
const val KEY_INTERNAL = "org.strongswan.android.resilience.INTERNAL"
|
||||
/** port used instead of the profile's for this connect */
|
||||
const val KEY_PORT_OVERRIDE = "org.strongswan.android.resilience.PORT"
|
||||
|
||||
/* the profile the user asked for and where the attempt stands */
|
||||
private var origin: String? = null
|
||||
private var portIndex = -1
|
||||
private var onReserve = false
|
||||
/* set while switched to the bridge: the profile to come back to */
|
||||
private var bridgeOrigin: String? = null
|
||||
|
||||
/** Every connect request; a user's connect starts a new attempt. */
|
||||
@JvmStatic
|
||||
fun onConnect(uuid: String?, info: Bundle)
|
||||
{
|
||||
if (info.getBoolean(KEY_INTERNAL, false))
|
||||
{
|
||||
return
|
||||
}
|
||||
origin = uuid
|
||||
portIndex = -1
|
||||
onReserve = false
|
||||
bridgeOrigin = null
|
||||
}
|
||||
|
||||
/**
|
||||
* The server of the current attempt does not answer. Returns true when
|
||||
* another attempt was started (the regular retry stays off).
|
||||
*/
|
||||
@JvmStatic
|
||||
fun onUnreachable(service: VpnStateService): Boolean
|
||||
{
|
||||
val current = service.profile ?: return false
|
||||
val start = origin ?: current.getUUID().toString().also { origin = it }
|
||||
val extra = ProfileExtras.get(service, start)
|
||||
if (!onReserve && portIndex + 1 < extra.backupPorts.size)
|
||||
{
|
||||
portIndex++
|
||||
val port = extra.backupPorts[portIndex]
|
||||
DiagLog.i(TAG, "server does not answer, trying backup port $port")
|
||||
connect(service, start, current.password.takeIf { current.getUUID().toString() == start }, port)
|
||||
return true
|
||||
}
|
||||
val reserve = extra.reserveUuid
|
||||
if (!onReserve && reserve != null && reserve != start)
|
||||
{
|
||||
val profile = load(service, reserve) ?: return false
|
||||
if (profile.vpnType.has(VpnType.VpnTypeFeature.USER_PASS) && profile.password == null)
|
||||
{ /* the reserve needs a password nobody can type in now */
|
||||
DiagLog.i(TAG, "reserve ${profile.name} needs a password, not switching")
|
||||
return false
|
||||
}
|
||||
onReserve = true
|
||||
DiagLog.i(TAG, "server does not answer, switching to the reserve ${profile.name}")
|
||||
EventLog.getInstance(service).record(
|
||||
EventKind.INFO, EventCategory.CONNECTION,
|
||||
service.getString(R.string.event_reserve_title, profile.name ?: profile.gateway),
|
||||
service.getString(R.string.event_reserve_text, load(service, start)?.name ?: ""), key = "reserve:$start",
|
||||
)
|
||||
connect(service, reserve, null, null)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Connected: tell when it took a backup port. */
|
||||
@JvmStatic
|
||||
fun onConnected(service: VpnStateService)
|
||||
{
|
||||
val start = origin ?: return
|
||||
if (!onReserve && portIndex >= 0)
|
||||
{
|
||||
val port = ProfileExtras.get(service, start).backupPorts.getOrNull(portIndex) ?: return
|
||||
EventLog.getInstance(service).record(
|
||||
EventKind.INFO, EventCategory.CONNECTION, service.getString(R.string.event_backup_port_title, port),
|
||||
service.getString(R.string.event_backup_port_text), key = "port:$start",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** The operator whitelists came or went (WhiteListManager). */
|
||||
@JvmStatic
|
||||
fun onWhitelist(context: Context, active: Boolean)
|
||||
{
|
||||
val service = VpnStateService.getLiveInstance() ?: return
|
||||
val current = service.profile?.getUUID()?.toString() ?: return
|
||||
if (active)
|
||||
{
|
||||
val extra = ProfileExtras.get(context, current)
|
||||
val bridge = extra.bridgeUuid?.takeIf { extra.bridgeEnabled && it != current } ?: return
|
||||
if (service.state == VpnStateService.State.DISABLED && service.errorState == VpnStateService.ErrorState.NO_ERROR)
|
||||
{ /* the VPN is off on purpose */
|
||||
return
|
||||
}
|
||||
val profile = load(context, bridge) ?: return
|
||||
if (profile.vpnType.has(VpnType.VpnTypeFeature.USER_PASS) && profile.password == null)
|
||||
{
|
||||
return
|
||||
}
|
||||
DiagLog.i(TAG, "whitelists on, switching to the bridge ${profile.name}")
|
||||
EventLog.getInstance(context).record(
|
||||
EventKind.INFO, EventCategory.CONNECTION, context.getString(R.string.event_bridge_on_title, profile.name ?: profile.gateway),
|
||||
context.getString(R.string.event_bridge_on_text), key = "bridge",
|
||||
)
|
||||
connect(service, bridge, null, null)
|
||||
bridgeOrigin = current
|
||||
}
|
||||
else
|
||||
{
|
||||
val back = bridgeOrigin ?: return
|
||||
bridgeOrigin = null
|
||||
if (service.state == VpnStateService.State.DISABLED && service.errorState == VpnStateService.ErrorState.NO_ERROR)
|
||||
{
|
||||
return
|
||||
}
|
||||
val profile = load(context, back) ?: return
|
||||
DiagLog.i(TAG, "whitelists off, back to ${profile.name}")
|
||||
EventLog.getInstance(context).resolve("bridge")
|
||||
EventLog.getInstance(context).record(
|
||||
EventKind.OK, EventCategory.CONNECTION, context.getString(R.string.event_bridge_off_title, profile.name ?: profile.gateway),
|
||||
context.getString(R.string.event_bridge_off_text), key = "bridge-off",
|
||||
)
|
||||
connect(service, back, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun connect(service: VpnStateService, uuid: String, password: String?, port: Int?)
|
||||
{
|
||||
val info = Bundle()
|
||||
info.putString(VpnProfileDataSource.KEY_UUID, uuid)
|
||||
password?.let { info.putString(VpnProfileDataSource.KEY_PASSWORD, it) }
|
||||
port?.let { info.putInt(KEY_PORT_OVERRIDE, it) }
|
||||
info.putBoolean(KEY_INTERNAL, true)
|
||||
service.connect(info, true)
|
||||
}
|
||||
|
||||
private fun load(context: Context, uuid: String) = runCatching {
|
||||
val source = VpnProfileSource(context)
|
||||
source.open()
|
||||
try
|
||||
{
|
||||
source.getVpnProfile(UUID.fromString(uuid))
|
||||
}
|
||||
finally
|
||||
{
|
||||
source.close()
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
@@ -1223,4 +1223,12 @@
|
||||
<string name="profile_update_keep">Останутся без изменений: запасные порты, резервный сервер, мост, DNS, правила и приложения.</string>
|
||||
<string name="profile_update_apply">Применить и переподключить</string>
|
||||
<string name="server_just_now">только что</string>
|
||||
<string name="event_reserve_title">Переключились на резерв «%1$s»</string>
|
||||
<string name="event_reserve_text">«%1$s» не ответил.</string>
|
||||
<string name="event_backup_port_title">Подключились через порт %1$d</string>
|
||||
<string name="event_backup_port_text">Основной порт не ответил, сработал запасной.</string>
|
||||
<string name="event_bridge_on_title">Включили мост «%1$s»</string>
|
||||
<string name="event_bridge_on_text">Оператор включил белые списки: российские сайты работают, зарубежные серверы — нет.</string>
|
||||
<string name="event_bridge_off_title">Вернулись на «%1$s»</string>
|
||||
<string name="event_bridge_off_text">Белые списки сняли.</string>
|
||||
</resources>
|
||||
@@ -1209,4 +1209,12 @@
|
||||
<string name="profile_update_keep">Unchanged: backup ports, reserve server, bridge, DNS, rules and apps.</string>
|
||||
<string name="profile_update_apply">Apply and reconnect</string>
|
||||
<string name="server_just_now">just now</string>
|
||||
<string name="event_reserve_title">Switched to the reserve «%1$s»</string>
|
||||
<string name="event_reserve_text">«%1$s» did not answer.</string>
|
||||
<string name="event_backup_port_title">Connected through port %1$d</string>
|
||||
<string name="event_backup_port_text">The main port did not answer, a backup port worked.</string>
|
||||
<string name="event_bridge_on_title">Switched to the bridge «%1$s»</string>
|
||||
<string name="event_bridge_on_text">The operator turned on whitelists: Russian sites work, foreign servers do not.</string>
|
||||
<string name="event_bridge_off_title">Back to «%1$s»</string>
|
||||
<string name="event_bridge_off_text">The whitelists are off.</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user