From 59afc6bcc7537a7f086c5e6f982adfa7d6f7d20c Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 15 May 2026 15:13:37 +0300 Subject: [PATCH] Guard Android VPN runtime generations --- clients/android/app/build.gradle | 4 +- .../java/su/cin/rapvpn/RapVpnService.java | 55 +++++++++++-------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/clients/android/app/build.gradle b/clients/android/app/build.gradle index 8c40e6f..6aeb710 100644 --- a/clients/android/app/build.gradle +++ b/clients/android/app/build.gradle @@ -30,8 +30,8 @@ android { applicationId "su.cin.rapvpn" minSdk 26 targetSdk 35 - versionCode 200 - versionName "0.2.200" + versionCode 201 + versionName "0.2.201" buildConfigField "String", "DEFAULT_BACKEND_URL", "\"${normalizeGradleString(defaultBackendUrl)}\"" buildConfigField "String", "DEFAULT_CLUSTER_ID", "\"${normalizeGradleString(defaultClusterId)}\"" buildConfigField "String", "DEFAULT_ORGANIZATION_ID", "\"${normalizeGradleString(defaultOrganizationId)}\"" diff --git a/clients/android/app/src/main/java/su/cin/rapvpn/RapVpnService.java b/clients/android/app/src/main/java/su/cin/rapvpn/RapVpnService.java index 1ccd83d..6ed040f 100644 --- a/clients/android/app/src/main/java/su/cin/rapvpn/RapVpnService.java +++ b/clients/android/app/src/main/java/su/cin/rapvpn/RapVpnService.java @@ -195,6 +195,7 @@ public class RapVpnService extends VpnService { private final AtomicLong runtimeWatchdogRecoveries = new AtomicLong(); private final AtomicLong tcpHandshakeStalls = new AtomicLong(); private final AtomicLong runtimeWatchdogHardRestarts = new AtomicLong(); + private final AtomicLong runtimeGeneration = new AtomicLong(); private final AtomicLong diagnosticEnsureRequests = new AtomicLong(); private final AtomicLong diagnosticRestartRequests = new AtomicLong(); private final AtomicBoolean hardRuntimeRestartInProgress = new AtomicBoolean(); @@ -1025,6 +1026,7 @@ public class RapVpnService extends VpnService { writeActivePacketRelayConfig(selectedRelayUrl, relayUrls); stopPacketRelay(); running = true; + long runtimeId = runtimeGeneration.incrementAndGet(); runtimeStartedAt = System.currentTimeMillis(); uplinkWorkerCount = Math.max(1, Math.min(UPLINK_WORKER_MAX_COUNT, Math.max(1, Runtime.getRuntime().availableProcessors() - 1))); if (uplinkWorkerCount < 2) { @@ -1073,15 +1075,15 @@ public class RapVpnService extends VpnService { writeRuntimeStatus("relay_reset_warning", "queue reset failed: " + e.getMessage(), 0, 0, 0, 1); } }, "rap-vpn-relay-reset"); - uplinkThread = new Thread(() -> pumpTunToRelay(clusterId, vpnConnectionId), "rap-vpn-uplink"); + uplinkThread = new Thread(() -> pumpTunToRelay(runtimeId, clusterId, vpnConnectionId), "rap-vpn-uplink"); uplinkSenderThreads = new Thread[uplinkWorkerCount]; for (int i = 0; i < uplinkWorkerCount; i++) { final int workerIndex = i; - uplinkSenderThreads[i] = new Thread(() -> pumpUplinkQueueToRelay(workerIndex, clusterId, vpnConnectionId), "rap-vpn-uplink-sender-" + workerIndex); + uplinkSenderThreads[i] = new Thread(() -> pumpUplinkQueueToRelay(runtimeId, workerIndex, clusterId, vpnConnectionId), "rap-vpn-uplink-sender-" + workerIndex); } - downlinkThread = new Thread(() -> runDownlinkWithRestart(clusterId, vpnConnectionId), "rap-vpn-downlink-receiver"); - downlinkWriterThread = new Thread(this::pumpDownlinkQueueToTun, "rap-vpn-downlink-writer"); - runtimeWatchdogThread = new Thread(() -> runRuntimeWatchdog(clusterId, vpnConnectionId), "rap-vpn-runtime-watchdog"); + downlinkThread = new Thread(() -> runDownlinkWithRestart(runtimeId, clusterId, vpnConnectionId), "rap-vpn-downlink-receiver"); + downlinkWriterThread = new Thread(() -> pumpDownlinkQueueToTun(runtimeId), "rap-vpn-downlink-writer"); + runtimeWatchdogThread = new Thread(() -> runRuntimeWatchdog(runtimeId, clusterId, vpnConnectionId), "rap-vpn-runtime-watchdog"); diagnosticWatchdogThread = new Thread(this::runDiagnosticServiceWatchdog, "rap-vpn-diagnostic-watchdog"); if (resetThread != null) { resetThread.start(); @@ -1308,6 +1310,7 @@ public class RapVpnService extends VpnService { } private void stopPacketRelay() { + runtimeGeneration.incrementAndGet(); running = false; VpnPacketWebSocketRelay relay = packetWebSocketRelay; packetWebSocketRelay = null; @@ -1564,13 +1567,17 @@ public class RapVpnService extends VpnService { } } - private void runDownlinkWithRestart(String clusterId, String vpnConnectionId) { + private boolean isRuntimeActive(long runtimeId) { + return running && runtimeGeneration.get() == runtimeId; + } + + private void runDownlinkWithRestart(long runtimeId, String clusterId, String vpnConnectionId) { long restarts = 0; - while (running) { + while (isRuntimeActive(runtimeId)) { downlinkRestarts = restarts; writeRuntimeDetail("starting", "downlink loop starting", "downlink", 0, restarts, ""); pumpRelayToTun(clusterId, vpnConnectionId); - if (!running) { + if (!isRuntimeActive(runtimeId)) { return; } restarts++; @@ -1580,18 +1587,18 @@ public class RapVpnService extends VpnService { try { Thread.sleep(100); } catch (InterruptedException e) { - if (!running) { + if (!isRuntimeActive(runtimeId)) { return; } } } } - private void runRuntimeWatchdog(String clusterId, String vpnConnectionId) { - while (running) { + private void runRuntimeWatchdog(long runtimeId, String clusterId, String vpnConnectionId) { + while (isRuntimeActive(runtimeId)) { try { Thread.sleep(RUNTIME_WATCHDOG_INTERVAL_MS); - if (!running) { + if (!isRuntimeActive(runtimeId)) { return; } long now = System.currentTimeMillis(); @@ -1645,7 +1652,7 @@ public class RapVpnService extends VpnService { recoverPacketRelayRuntime(clusterId, vpnConnectionId, "tcp_handshake_stall stale=" + stale); } } catch (InterruptedException e) { - if (!running) { + if (!isRuntimeActive(runtimeId)) { return; } } catch (Exception e) { @@ -1821,7 +1828,7 @@ public class RapVpnService extends VpnService { return value == null || value.isEmpty() ? "missing" : "present"; } - private void pumpTunToRelay(String clusterId, String vpnConnectionId) { + private void pumpTunToRelay(long runtimeId, String clusterId, String vpnConnectionId) { byte[] packet = new byte[32767]; long readPackets = 0; FileDescriptor fd = null; @@ -1831,7 +1838,7 @@ public class RapVpnService extends VpnService { input = new FileInputStream(fd); uplinkTunFd = fd; uplinkTunInput = input; - while (running) { + while (isRuntimeActive(runtimeId)) { int n = input.read(packet); if (n > 0) { readPackets++; @@ -1849,7 +1856,7 @@ public class RapVpnService extends VpnService { } } } catch (Exception e) { - if (running) { + if (isRuntimeActive(runtimeId)) { Log.e(TAG, "vpn uplink stopped", e); writeRuntimeStatus("error", "uplink stopped: " + e.getMessage(), readPackets, 0, 0, 0); writeRuntimeDetail("stopped", "uplink stopped: " + e.getMessage(), "uplink", readPackets, 0, e.getClass().getSimpleName()); @@ -2474,11 +2481,11 @@ public class RapVpnService extends VpnService { } } - private void pumpUplinkQueueToRelay(int workerIndex, String clusterId, String vpnConnectionId) { + private void pumpUplinkQueueToRelay(long runtimeId, int workerIndex, String clusterId, String vpnConnectionId) { long sentPackets = 0; long errors = 0; List batch = new ArrayList<>(VPN_BATCH_MAX_PACKETS); - while (running) { + while (isRuntimeActive(runtimeId)) { try { BlockingQueue[] queues = uplinkQueues; if (queues == null || workerIndex < 0 || workerIndex >= queues.length) { @@ -2556,12 +2563,12 @@ public class RapVpnService extends VpnService { writeRuntimeStatus("uplink_sent", "sent batch=" + batch.size(), 0, sentPackets, 0, errors); writeRuntimeDetail("sent", "worker=" + workerIndex + " sent batch=" + batch.size(), "uplink_sender", sentPackets, errors, "", workerIndex); } catch (InterruptedException e) { - if (!running) { + if (!isRuntimeActive(runtimeId)) { return; } writeRuntimeDetail("read_wait", "uplink queue wait interrupted", "uplink_sender", sentPackets, errors, e.getClass().getSimpleName(), workerIndex); } catch (Exception e) { - if (running) { + if (isRuntimeActive(runtimeId)) { Log.w(TAG, "vpn uplink batch send failed; continuing", e); errors++; AtomicLong[] senderErrors = uplinkSenderErrorsByWorker; @@ -2573,7 +2580,7 @@ public class RapVpnService extends VpnService { try { Thread.sleep(100); } catch (InterruptedException interrupted) { - if (!running) { + if (!isRuntimeActive(runtimeId)) { return; } } @@ -3191,7 +3198,7 @@ public class RapVpnService extends VpnService { return false; } - private void pumpDownlinkQueueToTun() { + private void pumpDownlinkQueueToTun(long runtimeId) { long writtenPackets = 0; long errors = 0; FileDescriptor fd = null; @@ -3199,7 +3206,7 @@ public class RapVpnService extends VpnService { try { fd = Os.dup(tunnel.getFileDescriptor()); downlinkTunFd = fd; - while (running) { + while (isRuntimeActive(runtimeId)) { BlockingQueue[] queues = downlinkQueues; if (queues == null || queues.length == 0) { return; @@ -3228,7 +3235,7 @@ public class RapVpnService extends VpnService { } } } catch (Exception e) { - if (running) { + if (isRuntimeActive(runtimeId)) { Log.e(TAG, "vpn downlink writer stopped", e); writeRuntimeStatus("error", "downlink writer stopped: " + e.getMessage(), 0, 0, writtenPackets, errors); writeRuntimeDetail("stopped", "downlink writer stopped: " + e.getMessage(), "downlink_writer", writtenPackets, errors, e.getClass().getSimpleName());