1
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -22,7 +22,7 @@ android {
|
||||
return (value == null ? "" : value.toString()).replace("\\", "\\\\").replace("\"", "\\\"")
|
||||
}
|
||||
|
||||
def defaultBackendUrl = project.findProperty("RAP_ANDROID_DEFAULT_BACKEND_URL") ?: "http://vpn.cin.su:19191/api/v1"
|
||||
def defaultBackendUrl = project.findProperty("RAP_ANDROID_DEFAULT_BACKEND_URL") ?: "https://vpn.cin.su/api/v1"
|
||||
def defaultClusterId = project.findProperty("RAP_ANDROID_DEFAULT_CLUSTER_ID") ?: "cfc0743d-d960-49fb-9de8-96e063d5e4aa"
|
||||
def defaultOrganizationId = project.findProperty("RAP_ANDROID_DEFAULT_ORGANIZATION_ID") ?: "125ff8b2-5ac1-4406-9bbb-ebbe18f7c7ed"
|
||||
|
||||
@@ -30,8 +30,8 @@ android {
|
||||
applicationId "su.cin.rapvpn"
|
||||
minSdk 26
|
||||
targetSdk 35
|
||||
versionCode 159
|
||||
versionName "0.2.159"
|
||||
versionCode 182
|
||||
versionName "0.2.182"
|
||||
buildConfigField "String", "DEFAULT_BACKEND_URL", "\"${normalizeGradleString(defaultBackendUrl)}\""
|
||||
buildConfigField "String", "DEFAULT_CLUSTER_ID", "\"${normalizeGradleString(defaultClusterId)}\""
|
||||
buildConfigField "String", "DEFAULT_ORGANIZATION_ID", "\"${normalizeGradleString(defaultOrganizationId)}\""
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.Locale;
|
||||
public class MainActivity extends Activity {
|
||||
private static final String APP_VERSION = BuildConfig.VERSION_NAME;
|
||||
private static final String DEFAULT_BACKEND_URL = BuildConfig.DEFAULT_BACKEND_URL;
|
||||
private static final String PUBLIC_FABRIC_BACKEND_URL = "https://vpn.cin.su/api/v1";
|
||||
private static final String DEFAULT_CLUSTER_ID = BuildConfig.DEFAULT_CLUSTER_ID;
|
||||
private static final String DEFAULT_ORGANIZATION_ID = BuildConfig.DEFAULT_ORGANIZATION_ID;
|
||||
private static final String PREF_SELECTED_EXIT_NODE_ID = "selected_exit_node_id";
|
||||
@@ -659,6 +660,16 @@ public class MainActivity extends Activity {
|
||||
if (candidate.isEmpty()) {
|
||||
return DEFAULT_BACKEND_URL;
|
||||
}
|
||||
String lower = candidate.toLowerCase(Locale.US);
|
||||
if ("http://vpn.cin.su:19191/api/v1".equals(lower)
|
||||
|| "http://vpn.cin.su/api/v1".equals(lower)
|
||||
|| "https://vpn.cin.su:443/api/v1".equals(lower)
|
||||
|| "http://94.141.118.222:19191/api/v1".equals(lower)
|
||||
|| "http://195.123.240.88:19131/api/v1".equals(lower)
|
||||
|| "http://192.168.200.61:18080/api/v1".equals(lower)
|
||||
|| "http://docker-test.cin.su:18080/api/v1".equals(lower)) {
|
||||
return PUBLIC_FABRIC_BACKEND_URL;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
|
||||
@@ -356,7 +356,7 @@ final class RapApiClient {
|
||||
return new byte[0];
|
||||
}
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IllegalStateException("HTTP " + response.code());
|
||||
throw new IllegalStateException(describeHttpFailure(response));
|
||||
}
|
||||
ResponseBody body = response.body();
|
||||
return body == null ? new byte[0] : body.bytes();
|
||||
@@ -377,15 +377,34 @@ final class RapApiClient {
|
||||
Request request = builder.build();
|
||||
try (Response response = httpClient.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IllegalStateException("HTTP " + response.code());
|
||||
throw new IllegalStateException(describeHttpFailure(response));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String clientPacketPath(String clusterId, String vpnConnectionId, String suffix) {
|
||||
private String describeHttpFailure(Response response) {
|
||||
StringBuilder message = new StringBuilder("HTTP ").append(response.code());
|
||||
ResponseBody body = response.body();
|
||||
if (body != null) {
|
||||
try {
|
||||
String text = body.string();
|
||||
if (text != null && !text.trim().isEmpty()) {
|
||||
text = text.replace('\n', ' ').replace('\r', ' ').trim();
|
||||
if (text.length() > 240) {
|
||||
text = text.substring(0, 240);
|
||||
}
|
||||
message.append(": ").append(text);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return message.toString();
|
||||
}
|
||||
|
||||
private String clientPacketPath(String clusterId, String vpnConnectionId, String suffix) throws IOException {
|
||||
String path = fabricServiceChannel.packetPathForBase(baseUrl, clusterId, vpnConnectionId, false);
|
||||
if (path.isEmpty()) {
|
||||
path = "/clusters/" + clusterId + "/vpn-connections/" + vpnConnectionId + "/tunnel/client/packets";
|
||||
throw new IOException("fabric service channel lease required for VPN packet dataplane");
|
||||
}
|
||||
return path + (suffix == null ? "" : suffix);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.provider.Settings;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.json.JSONObject;
|
||||
@@ -38,6 +39,7 @@ import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -51,6 +53,7 @@ public class RapDiagnosticService extends Service {
|
||||
private static final String CHANNEL_ID = "rap-vpn-diagnostics";
|
||||
private static final String APP_VERSION = BuildConfig.VERSION_NAME;
|
||||
private static final String DEFAULT_BACKEND_URL = BuildConfig.DEFAULT_BACKEND_URL;
|
||||
private static final String PUBLIC_FABRIC_BACKEND_URL = "https://vpn.cin.su/api/v1";
|
||||
private static final String INTERNAL_BACKEND_URL = "http://192.168.200.61:18080/api/v1";
|
||||
private static final String DEFAULT_CLUSTER_ID = BuildConfig.DEFAULT_CLUSTER_ID;
|
||||
private static final String DEFAULT_ORGANIZATION_ID = BuildConfig.DEFAULT_ORGANIZATION_ID;
|
||||
@@ -60,14 +63,22 @@ public class RapDiagnosticService extends Service {
|
||||
private static final String PREF_REFRESH_TOKEN = "refresh_token";
|
||||
private static final String PREF_USER_ID = "user_id";
|
||||
private static final String PREF_DEVICE_ID = "device_id";
|
||||
private static final String PREF_DIAGNOSTIC_DEVICE_ID = "diagnostic_device_id";
|
||||
private static final String PREF_PROFILE_JSON = "profile_json";
|
||||
private static final String PREF_VPN_CONNECTION_ID = "vpn_connection_id";
|
||||
private static final long COMMAND_STALE_MS = 45000;
|
||||
private static final long COMMAND_ORPHAN_MS = 60000;
|
||||
private static final long POLL_FORCE_MS = 45000;
|
||||
private volatile boolean running;
|
||||
private Thread worker;
|
||||
private Thread supervisor;
|
||||
private String serviceState = "";
|
||||
private String lastCommandType = "";
|
||||
private String lastCommandResult = "";
|
||||
private String lastCommandPollResult = "";
|
||||
private String lastReceivedCommandID = "";
|
||||
private String lastReceivedCommandType = "";
|
||||
private long lastReceivedCommandAt = 0;
|
||||
private long lastCommandAt = 0;
|
||||
private long lastHeartbeatAt = 0;
|
||||
private long lastCommandPollAt = 0;
|
||||
@@ -84,14 +95,14 @@ public class RapDiagnosticService extends Service {
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent != null && ACTION_STOP.equals(intent.getAction())) {
|
||||
running = false;
|
||||
if (worker != null) {
|
||||
worker.interrupt();
|
||||
}
|
||||
if (supervisor != null) {
|
||||
supervisor.interrupt();
|
||||
}
|
||||
stopForeground(true);
|
||||
stopSelfResult(startId);
|
||||
if (worker != null) {
|
||||
worker.interrupt();
|
||||
}
|
||||
if (supervisor != null) {
|
||||
supervisor.interrupt();
|
||||
}
|
||||
stopForeground(true);
|
||||
stopSelfResult(startId);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
if (intent != null && ACTION_RESTART.equals(intent.getAction())) {
|
||||
@@ -129,14 +140,24 @@ public class RapDiagnosticService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
static void restart(android.content.Context context) {
|
||||
Intent intent = new Intent(context, RapDiagnosticService.class);
|
||||
intent.setAction(ACTION_RESTART);
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
context.startForegroundService(intent);
|
||||
} else {
|
||||
context.startService(intent);
|
||||
}
|
||||
}
|
||||
|
||||
private void startWorker() {
|
||||
if (worker != null && worker.isAlive()) {
|
||||
long age = System.currentTimeMillis() - lastWorkerProgressAt;
|
||||
if (age > 45000) {
|
||||
restartWorker();
|
||||
} else {
|
||||
startSupervisor();
|
||||
return;
|
||||
startSupervisor();
|
||||
return;
|
||||
}
|
||||
}
|
||||
running = true;
|
||||
@@ -211,8 +232,12 @@ public class RapDiagnosticService extends Service {
|
||||
if (clusterId == null || clusterId.trim().isEmpty()) {
|
||||
clusterId = DEFAULT_CLUSTER_ID;
|
||||
}
|
||||
String deviceId = prefs.getString(PREF_DEVICE_ID, "");
|
||||
String deviceId = diagnosticDeviceId(prefs);
|
||||
if (backendUrl.isEmpty() || clusterId.isEmpty() || deviceId.isEmpty()) {
|
||||
serviceState = "waiting for config backend=" + !backendUrl.isEmpty()
|
||||
+ " cluster=" + !clusterId.isEmpty()
|
||||
+ " device=" + !deviceId.isEmpty();
|
||||
writeLocalDiagnosticHeartbeat();
|
||||
Thread.sleep(3000);
|
||||
continue;
|
||||
}
|
||||
@@ -259,12 +284,33 @@ public class RapDiagnosticService extends Service {
|
||||
commandPollStartedAt = 0;
|
||||
serviceState = "diagnostic poll watchdog released stale poll";
|
||||
}
|
||||
if (commandPollInProgress.get() && commandPollStartedAt == 0 && lastCommandPollAt > 0 && now - lastCommandPollAt > 45000) {
|
||||
commandPollInProgress.set(false);
|
||||
serviceState = "diagnostic poll watchdog released orphan poll flag age_ms=" + (now - lastCommandPollAt);
|
||||
}
|
||||
if (!commandPollInProgress.get() && !commandInProgress.get() && lastCommandPollAt > 0 && now - lastCommandPollAt > POLL_FORCE_MS) {
|
||||
serviceState = "diagnostic poll watchdog forcing command poll age_ms=" + (now - lastCommandPollAt);
|
||||
lastWorkerProgressAt = now;
|
||||
}
|
||||
long commandAge = commandInProgress.get() && commandStartedAt > 0 ? now - commandStartedAt : 0;
|
||||
if (commandAge > 120000) {
|
||||
if (commandAge > COMMAND_STALE_MS) {
|
||||
commandInProgress.set(false);
|
||||
commandStartedAt = 0;
|
||||
lastCommandType = lastReceivedCommandType.isEmpty() ? "command_timeout" : lastReceivedCommandType;
|
||||
lastCommandResult = "command watchdog timed out age_ms=" + commandAge
|
||||
+ " id=" + lastReceivedCommandID
|
||||
+ " type=" + lastReceivedCommandType;
|
||||
lastCommandAt = now;
|
||||
serviceState = "diagnostic command watchdog released stale command age_ms=" + commandAge;
|
||||
}
|
||||
if (commandInProgress.get() && commandStartedAt == 0 && lastCommandAt > 0 && now - lastCommandAt > COMMAND_ORPHAN_MS) {
|
||||
commandInProgress.set(false);
|
||||
serviceState = "diagnostic command watchdog released orphan command flag age_ms=" + (now - lastCommandAt);
|
||||
}
|
||||
if (commandInProgress.get() && commandStartedAt == 0 && lastCommandPollAt > 0 && now - lastCommandPollAt > COMMAND_ORPHAN_MS) {
|
||||
commandInProgress.set(false);
|
||||
serviceState = "diagnostic command watchdog released poll-stalled command flag age_ms=" + (now - lastCommandPollAt);
|
||||
}
|
||||
}
|
||||
|
||||
private void startHeartbeatWorker(String backendUrl, String clusterId, String deviceId, SharedPreferences prefs) {
|
||||
@@ -308,9 +354,14 @@ public class RapDiagnosticService extends Service {
|
||||
JSONObject commandEnvelope = nextCommandWithFallback(backendUrl, clusterId, deviceId);
|
||||
lastWorkerProgressAt = System.currentTimeMillis();
|
||||
if (commandEnvelope != null) {
|
||||
lastCommandPollResult = describeCommandEnvelope(commandEnvelope);
|
||||
rememberReceivedCommand(commandEnvelope);
|
||||
startCommandWorker(backendUrl, clusterId, deviceId, commandEnvelope);
|
||||
} else {
|
||||
lastCommandPollResult = "no_content";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
lastCommandPollResult = "error: " + e.getClass().getSimpleName();
|
||||
serviceState = "command poll error: " + e.getMessage();
|
||||
lastWorkerProgressAt = System.currentTimeMillis();
|
||||
} finally {
|
||||
@@ -323,11 +374,15 @@ public class RapDiagnosticService extends Service {
|
||||
|
||||
private void startCommandWorker(String backendUrl, String clusterId, String deviceId, JSONObject commandEnvelope) {
|
||||
if (!commandInProgress.compareAndSet(false, true)) {
|
||||
lastCommandPollResult = "worker_busy " + describeCommandEnvelope(commandEnvelope);
|
||||
return;
|
||||
}
|
||||
Thread commandWorker = new Thread(() -> {
|
||||
try {
|
||||
commandStartedAt = System.currentTimeMillis();
|
||||
lastCommandType = lastReceivedCommandType.isEmpty() ? "command_running" : lastReceivedCommandType;
|
||||
lastCommandResult = "running id=" + lastReceivedCommandID + " type=" + lastReceivedCommandType;
|
||||
lastCommandAt = commandStartedAt;
|
||||
lastWorkerProgressAt = System.currentTimeMillis();
|
||||
RapApiClient commandClient = controlClient(backendUrl);
|
||||
controlNetworkMode = commandClient.networkMode();
|
||||
@@ -363,6 +418,9 @@ public class RapDiagnosticService extends Service {
|
||||
params = payload;
|
||||
}
|
||||
String result;
|
||||
lastCommandType = type;
|
||||
lastCommandResult = "running id=" + (command == null ? "" : command.optString("id", "")) + " type=" + type;
|
||||
lastCommandAt = System.currentTimeMillis();
|
||||
if ("start_vpn".equals(type)) {
|
||||
result = startVPNFromSavedProfile();
|
||||
} else if ("stop_vpn".equals(type)) {
|
||||
@@ -411,6 +469,8 @@ public class RapDiagnosticService extends Service {
|
||||
result = "remote_assist_end accepted";
|
||||
} else if ("full_vpn_test".equals(type)) {
|
||||
result = runFullVPNTest(client, clusterId, params);
|
||||
} else if ("install_profile".equals(type) || "apply_profile".equals(type)) {
|
||||
result = installProfileFromCommand(params);
|
||||
} else if ("refresh_profile".equals(type)) {
|
||||
result = refreshProfile();
|
||||
} else {
|
||||
@@ -418,9 +478,15 @@ public class RapDiagnosticService extends Service {
|
||||
}
|
||||
if (isRecoverableVPNProbe(type) && looksLikeVPNStall(result)) {
|
||||
String firstResult = result;
|
||||
String recovery = controlledRestartVPNRuntime(client, clusterId);
|
||||
Thread.sleep(4000);
|
||||
result = firstResult + " | recovery=" + recovery + " | retry=" + runVPNProbeCommand(type, params);
|
||||
Thread.sleep(1500);
|
||||
String fastRetry = runVPNProbeCommand(type, params);
|
||||
if (!looksLikeVPNStall(fastRetry)) {
|
||||
result = firstResult + " | fast_retry=" + fastRetry;
|
||||
} else {
|
||||
String recovery = controlledRestartVPNRuntime(client, clusterId);
|
||||
Thread.sleep(4000);
|
||||
result = firstResult + " | fast_retry=" + fastRetry + " | recovery=" + recovery + " | retry=" + runVPNProbeCommand(type, params);
|
||||
}
|
||||
}
|
||||
lastCommandType = type;
|
||||
lastCommandResult = result;
|
||||
@@ -623,7 +689,9 @@ public class RapDiagnosticService extends Service {
|
||||
return "restart failed: queue reset failed: " + e.getMessage();
|
||||
}
|
||||
Thread.sleep(300);
|
||||
return startVPNFromSavedProfile();
|
||||
String refresh = refreshProfile();
|
||||
String start = startVPNFromSavedProfile();
|
||||
return start + " profile_refresh=" + refresh;
|
||||
} catch (Exception e) {
|
||||
return "restart failed: " + e.getClass().getSimpleName() + ": " + e.getMessage();
|
||||
}
|
||||
@@ -642,6 +710,10 @@ public class RapDiagnosticService extends Service {
|
||||
}
|
||||
serviceState = "upgrade restart " + lastVersion + " -> " + APP_VERSION;
|
||||
try {
|
||||
String refresh = refreshProfile();
|
||||
if (refresh.startsWith("refresh_profile failed")) {
|
||||
lastCommandResult = "vpn runtime profile refresh before upgrade restart failed: " + refresh;
|
||||
}
|
||||
try {
|
||||
client.resetVPNPacketQueues(clusterId, connectionId);
|
||||
} catch (Exception ignored) {
|
||||
@@ -650,7 +722,7 @@ public class RapDiagnosticService extends Service {
|
||||
startVPNFromSavedProfile();
|
||||
prefs.edit().putString("vpn_runtime_app_version", APP_VERSION).apply();
|
||||
lastCommandType = "auto_upgrade_restart";
|
||||
lastCommandResult = "vpn runtime reinitialized after app upgrade " + lastVersion + " -> " + APP_VERSION;
|
||||
lastCommandResult = "vpn runtime reinitialized after app upgrade " + lastVersion + " -> " + APP_VERSION + " profile_refresh=" + refresh;
|
||||
lastCommandAt = System.currentTimeMillis();
|
||||
} catch (Exception e) {
|
||||
lastCommandType = "auto_upgrade_restart";
|
||||
@@ -664,44 +736,135 @@ public class RapDiagnosticService extends Service {
|
||||
try {
|
||||
String refreshToken = new SecureTokenStore(this).get(PREF_REFRESH_TOKEN);
|
||||
if (refreshToken.isEmpty()) {
|
||||
return "refresh_profile skipped: refresh token missing";
|
||||
String savedUserId = prefs.getString(PREF_USER_ID, "");
|
||||
if (savedUserId == null || savedUserId.trim().isEmpty()) {
|
||||
return "refresh_profile skipped: refresh token and saved user missing";
|
||||
}
|
||||
return refreshProfileForUser(prefs, savedUserId.trim(), null);
|
||||
}
|
||||
RapApiClient client = new RapApiClient(normalizeBackendUrl(prefs.getString("backend_url", "")), this, true);
|
||||
RapApiClient.AuthContext auth = client.refresh(refreshToken);
|
||||
String organizationId = prefs.getString("organization_id", DEFAULT_ORGANIZATION_ID);
|
||||
String clusterId = prefs.getString("cluster_id", DEFAULT_CLUSTER_ID);
|
||||
if (clusterId == null || clusterId.trim().isEmpty()) {
|
||||
clusterId = DEFAULT_CLUSTER_ID;
|
||||
}
|
||||
if (organizationId == null || organizationId.trim().isEmpty()) {
|
||||
organizationId = DEFAULT_ORGANIZATION_ID;
|
||||
}
|
||||
String exitNodeId = prefs.getString(PREF_SELECTED_EXIT_NODE_ID, "");
|
||||
String profileJson = client.vpnClientProfile(clusterId, organizationId, auth.userId, exitNodeId);
|
||||
JSONObject root = new JSONObject(profileJson);
|
||||
JSONObject profile = root.getJSONObject("vpn_client_profile");
|
||||
String connectionId = profile.getJSONArray("connections").getJSONObject(0).getString("id");
|
||||
prefs.edit()
|
||||
.putString(PREF_USER_ID, auth.userId)
|
||||
.putString(PREF_DEVICE_ID, auth.deviceId)
|
||||
.putString(PREF_PROFILE_JSON, profileJson)
|
||||
.putString(PREF_VPN_CONNECTION_ID, connectionId)
|
||||
.apply();
|
||||
new SecureTokenStore(this).put(PREF_REFRESH_TOKEN, auth.refreshToken);
|
||||
return "refresh_profile ok " + connectionId;
|
||||
return refreshProfileForUser(prefs, auth.userId, auth.deviceId);
|
||||
} catch (Exception e) {
|
||||
return "refresh_profile failed: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private String refreshProfileForUser(SharedPreferences prefs, String userId, String trustedDeviceId) throws Exception {
|
||||
String backendUrl = normalizeBackendUrl(prefs.getString("backend_url", DEFAULT_BACKEND_URL));
|
||||
String organizationId = prefs.getString("organization_id", DEFAULT_ORGANIZATION_ID);
|
||||
String clusterId = prefs.getString("cluster_id", DEFAULT_CLUSTER_ID);
|
||||
if (clusterId == null || clusterId.trim().isEmpty()) {
|
||||
clusterId = DEFAULT_CLUSTER_ID;
|
||||
}
|
||||
if (organizationId == null || organizationId.trim().isEmpty()) {
|
||||
organizationId = DEFAULT_ORGANIZATION_ID;
|
||||
}
|
||||
String exitNodeId = prefs.getString(PREF_SELECTED_EXIT_NODE_ID, "");
|
||||
RapApiClient client = new RapApiClient(backendUrl, this, true);
|
||||
String profileJson = client.vpnClientProfile(clusterId, organizationId, userId, exitNodeId);
|
||||
JSONObject root = new JSONObject(profileJson);
|
||||
JSONObject profile = root.getJSONObject("vpn_client_profile");
|
||||
String connectionId = profile.getJSONArray("connections").getJSONObject(0).getString("id");
|
||||
SharedPreferences.Editor editor = prefs.edit()
|
||||
.putString("backend_url", backendUrl)
|
||||
.putString("cluster_id", clusterId)
|
||||
.putString("organization_id", organizationId)
|
||||
.putString(PREF_USER_ID, userId)
|
||||
.putString(PREF_PROFILE_JSON, profileJson)
|
||||
.putString(PREF_VPN_CONNECTION_ID, connectionId);
|
||||
if (trustedDeviceId != null && !trustedDeviceId.trim().isEmpty()) {
|
||||
editor.putString(PREF_DEVICE_ID, trustedDeviceId.trim());
|
||||
}
|
||||
editor.apply();
|
||||
return "refresh_profile ok " + connectionId;
|
||||
}
|
||||
|
||||
private String installProfileFromCommand(JSONObject params) {
|
||||
try {
|
||||
String backendUrl = normalizeBackendUrl(params.optString("backend_url", DEFAULT_BACKEND_URL));
|
||||
String clusterId = params.optString("cluster_id", DEFAULT_CLUSTER_ID).trim();
|
||||
String organizationId = params.optString("organization_id", DEFAULT_ORGANIZATION_ID).trim();
|
||||
String userId = params.optString("user_id", "").trim();
|
||||
String trustedDeviceId = params.optString("trusted_device_id", "").trim();
|
||||
String selectedExitNodeId = params.optString("selected_exit_node_id", params.optString("exit_node_id", "")).trim();
|
||||
String profileJson = params.optString("profile_json", "").trim();
|
||||
JSONObject root;
|
||||
if (profileJson.isEmpty()) {
|
||||
JSONObject profile = params.optJSONObject("vpn_client_profile");
|
||||
if (profile == null) {
|
||||
profile = params.optJSONObject("profile");
|
||||
}
|
||||
if (profile == null) {
|
||||
return "install_profile skipped: profile missing";
|
||||
}
|
||||
root = new JSONObject();
|
||||
root.put("vpn_client_profile", profile);
|
||||
profileJson = root.toString();
|
||||
} else {
|
||||
root = new JSONObject(profileJson);
|
||||
if (!root.has("vpn_client_profile")) {
|
||||
JSONObject wrapped = new JSONObject();
|
||||
wrapped.put("vpn_client_profile", root);
|
||||
root = wrapped;
|
||||
profileJson = wrapped.toString();
|
||||
}
|
||||
}
|
||||
JSONObject profile = root.getJSONObject("vpn_client_profile");
|
||||
if (clusterId.isEmpty()) {
|
||||
clusterId = profile.optString("cluster_id", DEFAULT_CLUSTER_ID);
|
||||
}
|
||||
if (organizationId.isEmpty()) {
|
||||
organizationId = profile.optString("organization_id", DEFAULT_ORGANIZATION_ID);
|
||||
}
|
||||
if (userId.isEmpty()) {
|
||||
userId = profile.optString("user_id", "");
|
||||
}
|
||||
JSONObject connection = profile.getJSONArray("connections").getJSONObject(0);
|
||||
String connectionId = params.optString("vpn_connection_id", connection.optString("id", "")).trim();
|
||||
JSONObject config = connection.optJSONObject("client_config");
|
||||
if (selectedExitNodeId.isEmpty() && config != null) {
|
||||
JSONObject route = config.optJSONObject("vpn_fabric_route");
|
||||
if (route != null) {
|
||||
selectedExitNodeId = route.optString("selected_exit_node_id", "");
|
||||
}
|
||||
}
|
||||
SharedPreferences.Editor editor = getSharedPreferences(PREFS, MODE_PRIVATE).edit()
|
||||
.putString("backend_url", backendUrl)
|
||||
.putString("cluster_id", clusterId)
|
||||
.putString("organization_id", organizationId)
|
||||
.putString(PREF_USER_ID, userId)
|
||||
.putString(PREF_PROFILE_JSON, profileJson)
|
||||
.putString(PREF_VPN_CONNECTION_ID, connectionId);
|
||||
if (!trustedDeviceId.isEmpty()) {
|
||||
editor.putString(PREF_DEVICE_ID, trustedDeviceId);
|
||||
}
|
||||
if (!selectedExitNodeId.isEmpty()) {
|
||||
editor.putString(PREF_SELECTED_EXIT_NODE_ID, selectedExitNodeId);
|
||||
}
|
||||
editor.apply();
|
||||
Intent stopIntent = new Intent(this, RapVpnService.class);
|
||||
stopIntent.setAction(RapVpnService.ACTION_STOP);
|
||||
startService(stopIntent);
|
||||
Thread.sleep(300);
|
||||
return "install_profile ok " + connectionId + " | " + startVPNFromSavedProfile();
|
||||
} catch (Exception e) {
|
||||
return "install_profile failed: " + e.getClass().getSimpleName() + ": " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject statusPayload(String event) throws Exception {
|
||||
SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
|
||||
String deviceId = diagnosticDeviceId(prefs);
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("event", event);
|
||||
payload.put("app_version", APP_VERSION);
|
||||
payload.put("service", "diagnostic");
|
||||
payload.put("user_id", prefs.getString(PREF_USER_ID, ""));
|
||||
payload.put("device_id", prefs.getString(PREF_DEVICE_ID, ""));
|
||||
payload.put("device_id", deviceId);
|
||||
payload.put("trusted_device_id", prefs.getString(PREF_DEVICE_ID, ""));
|
||||
payload.put("diagnostic_device_id", prefs.getString(PREF_DIAGNOSTIC_DEVICE_ID, ""));
|
||||
payload.put("organization_id", prefs.getString("organization_id", ""));
|
||||
payload.put("vpn_connection_id", prefs.getString(PREF_VPN_CONNECTION_ID, ""));
|
||||
payload.put("backend_url", prefs.getString("backend_url", ""));
|
||||
@@ -716,15 +879,86 @@ public class RapDiagnosticService extends Service {
|
||||
payload.put("last_command_at", lastCommandAt);
|
||||
payload.put("last_heartbeat_at", lastHeartbeatAt);
|
||||
payload.put("last_command_poll_at", lastCommandPollAt);
|
||||
payload.put("last_command_poll_result", lastCommandPollResult);
|
||||
payload.put("last_received_command_id", lastReceivedCommandID);
|
||||
payload.put("last_received_command_type", lastReceivedCommandType);
|
||||
payload.put("last_received_command_at", lastReceivedCommandAt);
|
||||
payload.put("heartbeat_in_progress", heartbeatInProgress.get());
|
||||
payload.put("heartbeat_started_at", heartbeatStartedAt);
|
||||
payload.put("command_poll_in_progress", commandPollInProgress.get());
|
||||
payload.put("command_poll_started_at", commandPollStartedAt);
|
||||
payload.put("command_in_progress", commandInProgress.get());
|
||||
payload.put("command_started_at", commandStartedAt);
|
||||
payload.put("browser_test", browserTestSnapshot());
|
||||
return payload;
|
||||
}
|
||||
|
||||
private String describeCommandEnvelope(JSONObject envelope) {
|
||||
if (envelope == null) {
|
||||
return "no_content";
|
||||
}
|
||||
JSONObject command = envelope.optJSONObject("vpn_client_diagnostic_command");
|
||||
JSONObject payload = command == null ? envelope.optJSONObject("payload") : command.optJSONObject("payload");
|
||||
String id = command == null ? "" : command.optString("id", "");
|
||||
String type = payload == null ? "" : payload.optString("type", "");
|
||||
String value = "received";
|
||||
if (!type.isEmpty()) {
|
||||
value += " " + type;
|
||||
}
|
||||
if (!id.isEmpty()) {
|
||||
value += " " + id;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private void rememberReceivedCommand(JSONObject envelope) {
|
||||
JSONObject command = envelope == null ? null : envelope.optJSONObject("vpn_client_diagnostic_command");
|
||||
JSONObject payload = command == null ? (envelope == null ? null : envelope.optJSONObject("payload")) : command.optJSONObject("payload");
|
||||
lastReceivedCommandID = command == null ? "" : command.optString("id", "");
|
||||
lastReceivedCommandType = payload == null ? "" : payload.optString("type", "");
|
||||
lastReceivedCommandAt = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private String diagnosticDeviceId(SharedPreferences prefs) {
|
||||
String trusted = prefs.getString(PREF_DEVICE_ID, "");
|
||||
if (trusted != null && !trusted.trim().isEmpty()) {
|
||||
return trusted.trim();
|
||||
}
|
||||
String cached = prefs.getString(PREF_DIAGNOSTIC_DEVICE_ID, "");
|
||||
if (cached != null && !cached.trim().isEmpty()) {
|
||||
return cached.trim();
|
||||
}
|
||||
String androidId = "";
|
||||
try {
|
||||
androidId = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
String seed = androidId == null || androidId.trim().isEmpty()
|
||||
? UUID.randomUUID().toString()
|
||||
: androidId.trim();
|
||||
String generated = "diag-" + seed.replaceAll("[^A-Za-z0-9_-]", "").toLowerCase();
|
||||
if (generated.length() > 80) {
|
||||
generated = generated.substring(0, 80);
|
||||
}
|
||||
prefs.edit().putString(PREF_DIAGNOSTIC_DEVICE_ID, generated).apply();
|
||||
return generated;
|
||||
}
|
||||
|
||||
private String normalizeBackendUrl(String value) {
|
||||
String candidate = value == null ? "" : value.trim().replaceAll("/+$", "");
|
||||
if (candidate.isEmpty()) {
|
||||
return DEFAULT_BACKEND_URL;
|
||||
}
|
||||
String lower = candidate.toLowerCase();
|
||||
if ("http://vpn.cin.su:19191/api/v1".equals(lower)
|
||||
|| "http://vpn.cin.su/api/v1".equals(lower)
|
||||
|| "https://vpn.cin.su:443/api/v1".equals(lower)
|
||||
|| "http://94.141.118.222:19191/api/v1".equals(lower)
|
||||
|| "http://195.123.240.88:19131/api/v1".equals(lower)
|
||||
|| "http://192.168.200.61:18080/api/v1".equals(lower)
|
||||
|| "http://docker-test.cin.su:18080/api/v1".equals(lower)) {
|
||||
return PUBLIC_FABRIC_BACKEND_URL;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
@@ -791,7 +1025,7 @@ public class RapDiagnosticService extends Service {
|
||||
if (!connectionId.isEmpty()) {
|
||||
report.put("packet_stats", client.vpnPacketStats(clusterId, connectionId));
|
||||
}
|
||||
client.reportVPNDiagnosticStatus(clusterId, getSharedPreferences(PREFS, MODE_PRIVATE).getString(PREF_DEVICE_ID, ""), report);
|
||||
client.reportVPNDiagnosticStatus(clusterId, diagnosticDeviceId(getSharedPreferences(PREFS, MODE_PRIVATE)), report);
|
||||
}
|
||||
if (!connectionId.isEmpty()) {
|
||||
result.append(" | stats=").append(compact(client.vpnPacketStats(clusterId, connectionId).toString(), 900));
|
||||
@@ -818,7 +1052,7 @@ public class RapDiagnosticService extends Service {
|
||||
|
||||
private String runVPNDownloadTest(String target) {
|
||||
try {
|
||||
Network vpn = vpnNetwork();
|
||||
Network vpn = waitForVPNNetwork(5000);
|
||||
if (vpn == null) {
|
||||
return "vpn_download_test " + target + " -> vpn network not found";
|
||||
}
|
||||
@@ -1009,6 +1243,13 @@ public class RapDiagnosticService extends Service {
|
||||
payload.put("diagnostic_local_heartbeat_at", runtime.getLong("diagnostic_local_heartbeat_at", 0));
|
||||
payload.put("diagnostic_local_state", runtime.getString("diagnostic_local_state", ""));
|
||||
payload.put("diagnostic_local_app_version", runtime.getString("diagnostic_local_app_version", ""));
|
||||
payload.put("diagnostic_watchdog_started_at", runtime.getLong("diagnostic_watchdog_started_at", 0));
|
||||
payload.put("diagnostic_watchdog_last_ensure_at", runtime.getLong("diagnostic_watchdog_last_ensure_at", 0));
|
||||
payload.put("diagnostic_watchdog_last_heartbeat_age_ms", runtime.getLong("diagnostic_watchdog_last_heartbeat_age_ms", 0));
|
||||
payload.put("diagnostic_watchdog_last_action", runtime.getString("diagnostic_watchdog_last_action", ""));
|
||||
payload.put("diagnostic_watchdog_last_error", runtime.getString("diagnostic_watchdog_last_error", ""));
|
||||
payload.put("diagnostic_watchdog_ensure_requests", runtime.getLong("diagnostic_watchdog_ensure_requests", 0));
|
||||
payload.put("diagnostic_watchdog_restart_requests", runtime.getLong("diagnostic_watchdog_restart_requests", 0));
|
||||
payload.put("uplink_read", runtime.getLong("uplink_read", 0));
|
||||
payload.put("uplink_sent", runtime.getLong("uplink_sent", 0));
|
||||
payload.put("downlink_received", runtime.getLong("downlink_received", 0));
|
||||
@@ -1044,8 +1285,10 @@ public class RapDiagnosticService extends Service {
|
||||
payload.put("errors", runtime.getLong("errors", 0));
|
||||
payload.put("uplink", runtimePrefix(runtime, "uplink"));
|
||||
payload.put("uplink_sender", runtimePrefix(runtime, "uplink_sender"));
|
||||
payload.put("uplink_tcp", runtimePrefix(runtime, "uplink_tcp"));
|
||||
payload.put("downlink", runtimePrefix(runtime, "downlink"));
|
||||
payload.put("downlink_writer", runtimePrefix(runtime, "downlink_writer"));
|
||||
payload.put("downlink_tcp", runtimePrefix(runtime, "downlink_tcp"));
|
||||
payload.put("relay", runtimePrefix(runtime, "relay"));
|
||||
payload.put("uplink_worker_count", runtime.getInt("uplink_worker_count", 0));
|
||||
payload.put("uplink_queue_depth_total", runtime.getInt("uplink_queue_depth_total", 0));
|
||||
@@ -1266,7 +1509,7 @@ public class RapDiagnosticService extends Service {
|
||||
}
|
||||
long started = System.currentTimeMillis();
|
||||
try {
|
||||
Network vpn = vpnNetwork();
|
||||
Network vpn = waitForVPNNetwork(5000);
|
||||
if (vpn == null) {
|
||||
return "vpn_tcp_connect " + host + ":" + port + " -> vpn network not found";
|
||||
}
|
||||
@@ -1283,6 +1526,24 @@ public class RapDiagnosticService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
private Network waitForVPNNetwork(int timeoutMs) {
|
||||
long deadline = System.currentTimeMillis() + Math.max(0, timeoutMs);
|
||||
Network vpn;
|
||||
do {
|
||||
vpn = vpnNetwork();
|
||||
if (vpn != null) {
|
||||
return vpn;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(150);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
} while (System.currentTimeMillis() < deadline);
|
||||
return null;
|
||||
}
|
||||
|
||||
private FetchResult fetchVPNURL(Network vpn, URL url, int connectTimeoutMs, int readTimeoutMs, int maxBytes) throws Exception {
|
||||
long started = System.currentTimeMillis();
|
||||
HttpURLConnection connection = (HttpURLConnection) vpn.openConnection(url);
|
||||
|
||||
@@ -26,7 +26,9 @@ import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -48,10 +50,12 @@ public class RapVpnService extends VpnService {
|
||||
private static final String CHANNEL_ID = "rap-vpn";
|
||||
private static final String TAG = "RapVpnService";
|
||||
private static final String PREFS = "rap-vpn-runtime";
|
||||
private static final int DEFAULT_VPN_MTU = 1420;
|
||||
private static final int DEFAULT_VPN_MTU = 1000;
|
||||
private static final int VPN_TCP_MSS_CLAMP = 900;
|
||||
private static final boolean PACKET_WEBSOCKET_DATAPLANE_ENABLED = false;
|
||||
private static final int VPN_BATCH_MAX_PACKETS = 512;
|
||||
private static final int VPN_BATCH_MAX_BYTES = 1024 * 1024;
|
||||
private static final int UPLINK_WORKER_MAX_COUNT = 4;
|
||||
private static final int UPLINK_WORKER_MAX_COUNT = 1;
|
||||
private static final int UPLINK_QUEUE_CAPACITY = 32768;
|
||||
private static final int PRIORITY_QUEUE_CAPACITY = 4096;
|
||||
private static final int UPLINK_SEND_RETRY_COUNT = 2;
|
||||
@@ -72,7 +76,12 @@ public class RapVpnService extends VpnService {
|
||||
private static final int RUNTIME_WATCHDOG_STALE_SYNACK_MS = 15000;
|
||||
private static final int RUNTIME_WATCHDOG_RECOVERY_COOLDOWN_MS = 60000;
|
||||
private static final int RUNTIME_WATCHDOG_HARD_RESTART_COOLDOWN_MS = 180000;
|
||||
private static final int DIAGNOSTIC_WATCHDOG_INTERVAL_MS = 5000;
|
||||
private static final int DIAGNOSTIC_STALE_RESTART_MS = 30000;
|
||||
private static final int DIAGNOSTIC_RESTART_COOLDOWN_MS = 15000;
|
||||
private static final int VPN_START_WARMUP_TIMEOUT_MS = 6000;
|
||||
private static final int VPN_TCP_WARMUP_CONNECT_TIMEOUT_MS = 2500;
|
||||
private static final int VPN_TCP_WARMUP_PASSES = 3;
|
||||
private static final String[] DEFAULT_DNS_PROBE_DOMAINS = new String[]{
|
||||
"speedtest.rt.ru",
|
||||
"2ip.ru",
|
||||
@@ -97,6 +106,11 @@ public class RapVpnService extends VpnService {
|
||||
"rline-host.qms.ru",
|
||||
"timernet-host.qms.ru"
|
||||
};
|
||||
private static final String[] DEFAULT_TCP_WARMUP_TARGETS = new String[]{
|
||||
"192.168.200.61:18080",
|
||||
"192.168.200.95:3389",
|
||||
"188.40.167.82:80"
|
||||
};
|
||||
private static final String PREF_NAME = "rap-vpn";
|
||||
private static final String PREF_PROFILE_JSON = "profile_json";
|
||||
private static final String PREF_BACKEND_URL = "backend_url";
|
||||
@@ -112,6 +126,7 @@ public class RapVpnService extends VpnService {
|
||||
private Thread downlinkThread;
|
||||
private Thread downlinkWriterThread;
|
||||
private Thread runtimeWatchdogThread;
|
||||
private Thread diagnosticWatchdogThread;
|
||||
private BlockingQueue<byte[]>[] uplinkQueues;
|
||||
private BlockingQueue<byte[]>[] downlinkQueues;
|
||||
private BlockingQueue<byte[]> uplinkPriorityQueue;
|
||||
@@ -171,6 +186,8 @@ 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 diagnosticEnsureRequests = new AtomicLong();
|
||||
private final AtomicLong diagnosticRestartRequests = new AtomicLong();
|
||||
private final AtomicBoolean hardRuntimeRestartInProgress = new AtomicBoolean();
|
||||
private volatile boolean relaxedUplinkSourceValidation;
|
||||
private volatile boolean relaxedDownlinkDestinationValidation;
|
||||
@@ -180,6 +197,7 @@ public class RapVpnService extends VpnService {
|
||||
private volatile int activePacketRelayIndex;
|
||||
private volatile VpnPacketWebSocketRelay packetWebSocketRelay;
|
||||
private volatile FabricServiceChannel activeFabricServiceChannel = new FabricServiceChannel();
|
||||
private volatile String lastUplinkSendErrorMessage = "";
|
||||
private final Object packetRelaySwitchLock = new Object();
|
||||
private final Map<String, byte[]> clientSourceNat = new LinkedHashMap<String, byte[]>(4096, 0.75f, true) {
|
||||
@Override
|
||||
@@ -198,7 +216,7 @@ public class RapVpnService extends VpnService {
|
||||
private volatile long lastRuntimeWatchdogHardRestartAt;
|
||||
private volatile long lastDiagnosticEnsureAt;
|
||||
private volatile long lastDiagnosticStatusEnsureAt;
|
||||
private volatile boolean nextDiagnosticEnsureMayRestart;
|
||||
private volatile long lastDiagnosticRestartAt;
|
||||
private static final int ADDRESS_MISMATCH_TOLERANCE_PACKETS = 16;
|
||||
|
||||
@Override
|
||||
@@ -257,8 +275,17 @@ public class RapVpnService extends VpnService {
|
||||
List<String> packetRelayUrls = activePacketRelayUrlsByProfile == null || activePacketRelayUrlsByProfile.isEmpty()
|
||||
? singletonUrl(activePacketRelayUrlByProfile)
|
||||
: new ArrayList<>(activePacketRelayUrlsByProfile);
|
||||
if (!activeFabricServiceChannel.enabled) {
|
||||
shutdownReason = "fabric service channel lease required";
|
||||
writeRuntimeStatus("error", "vpn not started: fabric service channel lease required", 0, 0, 0, 0);
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
if (packetRelayUrls.isEmpty()) {
|
||||
packetRelayUrls.add(backendUrl);
|
||||
shutdownReason = "missing farm entry endpoint";
|
||||
writeRuntimeStatus("error", "vpn not started: missing farm entry endpoint", 0, 0, 0, 0);
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
startPacketRelay(backendUrl, packetRelayUrls, clusterId, vpnConnectionId);
|
||||
if (!running) {
|
||||
@@ -284,6 +311,7 @@ public class RapVpnService extends VpnService {
|
||||
private void ensureDiagnosticServiceRunning() {
|
||||
try {
|
||||
RapDiagnosticService.start(this);
|
||||
diagnosticEnsureRequests.incrementAndGet();
|
||||
writeRuntimeDetail("diagnostic_start", "diagnostic service start requested by vpn runtime", "control", 0, 0, "", -1);
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "diagnostic service start failed", e);
|
||||
@@ -295,8 +323,15 @@ public class RapVpnService extends VpnService {
|
||||
try {
|
||||
SharedPreferences runtime = getSharedPreferences(PREFS, MODE_PRIVATE);
|
||||
long lastLocalHeartbeat = runtime.getLong("diagnostic_local_heartbeat_at", 0);
|
||||
long age = lastLocalHeartbeat <= 0 ? Long.MAX_VALUE : System.currentTimeMillis() - lastLocalHeartbeat;
|
||||
boolean restart = nextDiagnosticEnsureMayRestart && age > 45000;
|
||||
long now = System.currentTimeMillis();
|
||||
long age = lastLocalHeartbeat <= 0 ? Long.MAX_VALUE : now - lastLocalHeartbeat;
|
||||
boolean restart = age > DIAGNOSTIC_STALE_RESTART_MS
|
||||
&& now - lastDiagnosticRestartAt >= DIAGNOSTIC_RESTART_COOLDOWN_MS;
|
||||
diagnosticEnsureRequests.incrementAndGet();
|
||||
if (restart) {
|
||||
diagnosticRestartRequests.incrementAndGet();
|
||||
lastDiagnosticRestartAt = now;
|
||||
}
|
||||
Intent intent = new Intent(this, RapDiagnosticService.class);
|
||||
intent.setAction(restart ? RapDiagnosticService.ACTION_RESTART : RapDiagnosticService.ACTION_START);
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
@@ -304,7 +339,13 @@ public class RapVpnService extends VpnService {
|
||||
} else {
|
||||
startService(intent);
|
||||
}
|
||||
nextDiagnosticEnsureMayRestart = true;
|
||||
runtime.edit()
|
||||
.putLong("diagnostic_watchdog_last_ensure_at", now)
|
||||
.putLong("diagnostic_watchdog_last_heartbeat_age_ms", age)
|
||||
.putString("diagnostic_watchdog_last_action", restart ? "restart" : "start")
|
||||
.putLong("diagnostic_watchdog_ensure_requests", diagnosticEnsureRequests.get())
|
||||
.putLong("diagnostic_watchdog_restart_requests", diagnosticRestartRequests.get())
|
||||
.apply();
|
||||
writeRuntimeDetail(
|
||||
restart ? "diagnostic_restart" : "diagnostic_start",
|
||||
(restart ? "diagnostic service restart requested age_ms=" : "diagnostic service start requested age_ms=") + age,
|
||||
@@ -315,6 +356,11 @@ public class RapVpnService extends VpnService {
|
||||
-1);
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "diagnostic service health ensure failed", e);
|
||||
getSharedPreferences(PREFS, MODE_PRIVATE).edit()
|
||||
.putLong("diagnostic_watchdog_last_ensure_at", System.currentTimeMillis())
|
||||
.putString("diagnostic_watchdog_last_action", "failed")
|
||||
.putString("diagnostic_watchdog_last_error", e.getClass().getSimpleName() + ": " + e.getMessage())
|
||||
.apply();
|
||||
writeRuntimeDetail("diagnostic_start_failed", e.getMessage(), "control", 0, 1, e.getClass().getSimpleName(), -1);
|
||||
}
|
||||
}
|
||||
@@ -775,6 +821,19 @@ public class RapVpnService extends VpnService {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isIPv6URLHost(String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
URI uri = URI.create(value.trim());
|
||||
String host = uri.getHost();
|
||||
return host != null && host.contains(":");
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeRuntimeConfig(VpnClientConfig config, boolean forceFullTunnel, boolean fastPathMode) {
|
||||
try {
|
||||
getSharedPreferences(PREFS, MODE_PRIVATE).edit()
|
||||
@@ -935,11 +994,15 @@ public class RapVpnService extends VpnService {
|
||||
+ " connection=" + present(vpnConnectionId), 0, 0, 0, 0);
|
||||
return;
|
||||
}
|
||||
List<String> relayUrls = dedupeRelayUrls(candidateUrls, backendUrl);
|
||||
List<String> relayUrls = activeFabricServiceChannel.enabled ? dedupeFabricRelayUrls(candidateUrls, backendUrl) : dedupeRelayUrls(candidateUrls, backendUrl);
|
||||
String selectedRelayUrl = relayUrls.isEmpty() ? "" : relayUrls.get(0);
|
||||
if (selectedRelayUrl == null || selectedRelayUrl.isEmpty()) {
|
||||
if ((selectedRelayUrl == null || selectedRelayUrl.isEmpty()) && !activeFabricServiceChannel.enabled) {
|
||||
selectedRelayUrl = backendUrl;
|
||||
}
|
||||
if (selectedRelayUrl == null || selectedRelayUrl.isEmpty()) {
|
||||
writeRuntimeStatus("error", "relay not started: missing fabric farm entry endpoint", 0, 0, 0, 0);
|
||||
return;
|
||||
}
|
||||
activePacketRelayUrlByProfile = selectedRelayUrl;
|
||||
activePacketRelayUrlsByProfile = new ArrayList<>(relayUrls);
|
||||
activePacketRelayIndex = Math.max(0, relayUrls.indexOf(selectedRelayUrl));
|
||||
@@ -971,14 +1034,18 @@ public class RapVpnService extends VpnService {
|
||||
downlinkQueues[i] = new ArrayBlockingQueue<>(downlinkPerFlowCapacity);
|
||||
}
|
||||
configureBackendBypass(selectedRelayUrl);
|
||||
startPacketWebSocketRelay(selectedRelayUrl, clusterId, vpnConnectionId);
|
||||
if (PACKET_WEBSOCKET_DATAPLANE_ENABLED) {
|
||||
startPacketWebSocketRelay(selectedRelayUrl, clusterId, vpnConnectionId);
|
||||
} else {
|
||||
writeRuntimeDetail("http_packet_batch", "packet websocket disabled; using confirmed HTTP batches", "relay", 0, 0, "", -1);
|
||||
}
|
||||
Log.i(TAG, "packet relay starting: backend=" + selectedRelayUrl + " cluster=" + clusterId + " vpn_connection=" + vpnConnectionId);
|
||||
writeRuntimeStatus("relay", "relay starting " + vpnConnectionId, 0, 0, 0, 0);
|
||||
writeRuntimeDetail("running", "packet relay active", "relay", 0, 0, "");
|
||||
final String resetRelayUrl = selectedRelayUrl;
|
||||
Thread resetThread = new Thread(() -> {
|
||||
final String legacyResetRelayUrl = selectedRelayUrl;
|
||||
Thread resetThread = activeFabricServiceChannel.enabled ? null : new Thread(() -> {
|
||||
try {
|
||||
RapApiClient uplinkClient = packetRelayClientForUrl(resetRelayUrl);
|
||||
RapApiClient uplinkClient = packetRelayClientForUrl(legacyResetRelayUrl);
|
||||
JSONObject reset = uplinkClient.resetVPNPacketQueues(clusterId, vpnConnectionId);
|
||||
Log.i(TAG, "packet relay queues reset: " + reset.toString());
|
||||
writeRuntimeStatus("relay_reset", reset.toString(), 0, 0, 0, 0);
|
||||
@@ -996,7 +1063,12 @@ public class RapVpnService extends VpnService {
|
||||
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");
|
||||
resetThread.start();
|
||||
diagnosticWatchdogThread = new Thread(this::runDiagnosticServiceWatchdog, "rap-vpn-diagnostic-watchdog");
|
||||
if (resetThread != null) {
|
||||
resetThread.start();
|
||||
} else {
|
||||
writeRuntimeStatus("farm_dataplane", "backend relay queue reset skipped; farm owns vpn packet routes", 0, 0, 0, 0);
|
||||
}
|
||||
uplinkThread.start();
|
||||
for (Thread senderThread : uplinkSenderThreads) {
|
||||
senderThread.start();
|
||||
@@ -1004,6 +1076,7 @@ public class RapVpnService extends VpnService {
|
||||
downlinkThread.start();
|
||||
downlinkWriterThread.start();
|
||||
runtimeWatchdogThread.start();
|
||||
diagnosticWatchdogThread.start();
|
||||
}
|
||||
|
||||
private List<String> singletonUrl(String value) {
|
||||
@@ -1057,6 +1130,33 @@ public class RapVpnService extends VpnService {
|
||||
return out;
|
||||
}
|
||||
|
||||
private List<String> dedupeFabricRelayUrls(List<String> candidateUrls, String backendUrl) {
|
||||
List<String> ipv4OrHost = new ArrayList<>();
|
||||
List<String> ipv6 = new ArrayList<>();
|
||||
boolean backendIsPrivate = isPrivateURLHost(backendUrl);
|
||||
if (candidateUrls != null) {
|
||||
for (String url : candidateUrls) {
|
||||
String normalized = normalizeHTTPBaseUrl(url);
|
||||
if (!backendIsPrivate && isPrivateURLHost(normalized)) {
|
||||
continue;
|
||||
}
|
||||
if (isIPv6URLHost(normalized)) {
|
||||
addUniqueUrl(ipv6, normalized);
|
||||
} else {
|
||||
addUniqueUrl(ipv4OrHost, normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
List<String> out = new ArrayList<>();
|
||||
for (String url : ipv4OrHost) {
|
||||
addUniqueUrl(out, url);
|
||||
}
|
||||
for (String url : ipv6) {
|
||||
addUniqueUrl(out, url);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private void addUniqueUrl(List<String> urls, String value) {
|
||||
if (urls == null || value == null) {
|
||||
return;
|
||||
@@ -1132,6 +1232,9 @@ public class RapVpnService extends VpnService {
|
||||
if (next == null || next.isEmpty() || next.equals(normalizedFailed)) {
|
||||
continue;
|
||||
}
|
||||
if (isIPv6URLHost(next) && hasNonIPv6RelayUrl(urls)) {
|
||||
continue;
|
||||
}
|
||||
activePacketRelayIndex = nextIndex;
|
||||
activePacketRelayUrlByProfile = next;
|
||||
configureBackendBypass(next);
|
||||
@@ -1145,6 +1248,18 @@ public class RapVpnService extends VpnService {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasNonIPv6RelayUrl(List<String> urls) {
|
||||
if (urls == null) {
|
||||
return false;
|
||||
}
|
||||
for (String url : urls) {
|
||||
if (url != null && !url.isEmpty() && !isIPv6URLHost(url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String selectReachablePacketRelayUrl(List<String> relayUrls, String clusterId, String vpnConnectionId) {
|
||||
if (relayUrls == null || relayUrls.isEmpty()) {
|
||||
return "";
|
||||
@@ -1190,11 +1305,13 @@ public class RapVpnService extends VpnService {
|
||||
interruptAndJoin(downlinkThread);
|
||||
interruptAndJoin(downlinkWriterThread);
|
||||
interruptAndJoin(runtimeWatchdogThread);
|
||||
interruptAndJoin(diagnosticWatchdogThread);
|
||||
uplinkThread = null;
|
||||
uplinkSenderThreads = null;
|
||||
downlinkThread = null;
|
||||
downlinkWriterThread = null;
|
||||
runtimeWatchdogThread = null;
|
||||
diagnosticWatchdogThread = null;
|
||||
uplinkWorkerCount = 0;
|
||||
downlinkFlowQueueCount = 0;
|
||||
uplinkQueues = null;
|
||||
@@ -1411,10 +1528,6 @@ public class RapVpnService extends VpnService {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastDiagnosticEnsureAt >= 10000) {
|
||||
lastDiagnosticEnsureAt = now;
|
||||
ensureDiagnosticServiceHealthy();
|
||||
}
|
||||
int stale = staleTCPHandshakeCount();
|
||||
if (stale <= 0) {
|
||||
continue;
|
||||
@@ -1440,6 +1553,29 @@ public class RapVpnService extends VpnService {
|
||||
}
|
||||
}
|
||||
|
||||
private void runDiagnosticServiceWatchdog() {
|
||||
getSharedPreferences(PREFS, MODE_PRIVATE).edit()
|
||||
.putLong("diagnostic_watchdog_started_at", System.currentTimeMillis())
|
||||
.putString("diagnostic_watchdog_last_action", "started")
|
||||
.apply();
|
||||
while (running) {
|
||||
try {
|
||||
ensureDiagnosticServiceHealthy();
|
||||
Thread.sleep(DIAGNOSTIC_WATCHDOG_INTERVAL_MS);
|
||||
} catch (InterruptedException e) {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getSharedPreferences(PREFS, MODE_PRIVATE).edit()
|
||||
.putLong("diagnostic_watchdog_last_ensure_at", System.currentTimeMillis())
|
||||
.putString("diagnostic_watchdog_last_action", "failed")
|
||||
.putString("diagnostic_watchdog_last_error", e.getClass().getSimpleName() + ": " + e.getMessage())
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldHardRestartRuntime(long now) {
|
||||
if (runtimeWatchdogRecoveries.get() < 2) {
|
||||
return false;
|
||||
@@ -1636,14 +1772,13 @@ public class RapVpnService extends VpnService {
|
||||
System.arraycopy(packet, 0, copy, 0, length);
|
||||
if (!hasIPv4Source(copy, length)) {
|
||||
long mismatch = uplinkSourceMismatchPackets.incrementAndGet();
|
||||
String natKey = natKeyForOutboundReturn(copy, length);
|
||||
if (natKey.isEmpty() || !rewriteIPv4SourceToVPN(copy, length, natKey)) {
|
||||
Log.w(TAG, "vpn uplink source is not vpn address; dropping " + packetSummary(copy, length));
|
||||
writeRuntimeDetail("source_drop", packetSummary(copy, length), "uplink", -1, mismatch, "SOURCE_MISMATCH");
|
||||
recordUplinkDrop(length);
|
||||
return;
|
||||
}
|
||||
writeRuntimeDetail("source_nat", packetSummary(copy, length), "uplink", -1, mismatch, "SOURCE_NAT");
|
||||
Log.w(TAG, "vpn uplink source is not vpn address; dropping " + packetSummary(copy, length));
|
||||
writeRuntimeDetail("source_drop", packetSummary(copy, length), "uplink", -1, mismatch, "SOURCE_MISMATCH");
|
||||
recordUplinkDrop(length);
|
||||
return;
|
||||
}
|
||||
if (clampIPv4TCPMSS(copy, length, VPN_TCP_MSS_CLAMP)) {
|
||||
writeRuntimeDetail("tcp_mss_clamp", packetSummary(copy, length), "uplink_tcp", -1, -1, "");
|
||||
}
|
||||
recordOutboundTCPHandshake(copy, length);
|
||||
if (handleLocalDnsQuery(copy, length)) {
|
||||
@@ -1989,6 +2124,7 @@ public class RapVpnService extends VpnService {
|
||||
return;
|
||||
}
|
||||
String key = tcpFlowKey(flow.srcIp, flow.srcPort, flow.dstPort);
|
||||
writeRuntimeDetail("tcp_syn_ack", key, "downlink_tcp", downlinkReceivedPackets.get(), tcpHandshakeStalls.get(), "");
|
||||
synchronized (pendingTcpHandshakes) {
|
||||
if (pendingTcpHandshakes.containsKey(key)) {
|
||||
pendingTcpHandshakes.put(key, -System.currentTimeMillis());
|
||||
@@ -2014,7 +2150,11 @@ public class RapVpnService extends VpnService {
|
||||
}
|
||||
|
||||
private boolean isTCPPriorityPacket(byte[] packet, int length) {
|
||||
if (packet == null || length < 40 || ((packet[0] >> 4) & 0x0f) != 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean clampIPv4TCPMSS(byte[] packet, int length, int maxMss) {
|
||||
if (packet == null || length < 40 || maxMss <= 0 || ((packet[0] >> 4) & 0x0f) != 4) {
|
||||
return false;
|
||||
}
|
||||
int ihl = (packet[0] & 0x0f) * 4;
|
||||
@@ -2025,17 +2165,47 @@ public class RapVpnService extends VpnService {
|
||||
if (totalLength <= 0 || totalLength > length) {
|
||||
totalLength = length;
|
||||
}
|
||||
int tcpHeaderLength = ((packet[ihl + 12] >> 4) & 0x0f) * 4;
|
||||
if (tcpHeaderLength < 20 || ihl + tcpHeaderLength > totalLength) {
|
||||
int tcpOffset = ihl;
|
||||
int tcpHeaderLength = ((packet[tcpOffset + 12] >> 4) & 0x0f) * 4;
|
||||
if (tcpHeaderLength < 20 || tcpOffset + tcpHeaderLength > totalLength) {
|
||||
return false;
|
||||
}
|
||||
int flags = packet[ihl + 13] & 0xff;
|
||||
int flags = packet[tcpOffset + 13] & 0xff;
|
||||
boolean syn = (flags & 0x02) != 0;
|
||||
boolean fin = (flags & 0x01) != 0;
|
||||
boolean rst = (flags & 0x04) != 0;
|
||||
boolean ack = (flags & 0x10) != 0;
|
||||
int payloadLength = totalLength - ihl - tcpHeaderLength;
|
||||
return syn || fin || rst || (ack && payloadLength == 0);
|
||||
if (!syn || ack || tcpHeaderLength <= 20) {
|
||||
return false;
|
||||
}
|
||||
int option = tcpOffset + 20;
|
||||
int end = tcpOffset + tcpHeaderLength;
|
||||
while (option < end) {
|
||||
int kind = packet[option] & 0xff;
|
||||
if (kind == 0) {
|
||||
break;
|
||||
}
|
||||
if (kind == 1) {
|
||||
option++;
|
||||
continue;
|
||||
}
|
||||
if (option + 1 >= end) {
|
||||
break;
|
||||
}
|
||||
int optionLength = packet[option + 1] & 0xff;
|
||||
if (optionLength < 2 || option + optionLength > end) {
|
||||
break;
|
||||
}
|
||||
if (kind == 2 && optionLength == 4) {
|
||||
int current = u16(packet, option + 2);
|
||||
if (current > maxMss) {
|
||||
putU16(packet, option + 2, maxMss);
|
||||
normalizeIPv4PacketChecksums(packet, length);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
option += optionLength;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String tcpFlowKey(String remoteIp, int remotePort, int localPort) {
|
||||
@@ -2246,7 +2416,8 @@ public class RapVpnService extends VpnService {
|
||||
}
|
||||
recordUplinkDrop(Math.max(0, batchBytes - 4));
|
||||
writeRuntimeStatus("degraded", "uplink send failed after retry; continuing", 0, sentPackets, 0, errors);
|
||||
writeRuntimeDetail("error", "uplink send failed after retry batch=" + batch.size(), "uplink_sender", sentPackets, errors, "SEND_RETRY_EXHAUSTED", workerIndex);
|
||||
String retryError = lastUplinkSendErrorMessage == null || lastUplinkSendErrorMessage.isEmpty() ? "" : " last_error=" + lastUplinkSendErrorMessage;
|
||||
writeRuntimeDetail("error", "uplink send failed after retry batch=" + batch.size() + retryError, "uplink_sender", sentPackets, errors, "SEND_RETRY_EXHAUSTED", workerIndex);
|
||||
continue;
|
||||
}
|
||||
sentPackets += batch.size();
|
||||
@@ -2289,6 +2460,7 @@ public class RapVpnService extends VpnService {
|
||||
|
||||
private boolean sendUplinkBatchWithRetry(String clusterId, String vpnConnectionId, List<byte[]> batch, int workerIndex) {
|
||||
Exception lastError = null;
|
||||
lastUplinkSendErrorMessage = "";
|
||||
int relayAttempts = Math.max(1, activePacketRelayUrlsByProfile == null ? 1 : activePacketRelayUrlsByProfile.size());
|
||||
for (int relayAttempt = 0; relayAttempt < relayAttempts && running; relayAttempt++) {
|
||||
String relayUrl = currentPacketRelayUrl();
|
||||
@@ -2308,7 +2480,8 @@ public class RapVpnService extends VpnService {
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
lastError = e;
|
||||
writeRuntimeDetail("retry", "uplink send retry worker=" + workerIndex + " relay=" + relayUrl + " attempt=" + attempt + " error=" + e.getClass().getSimpleName(), "uplink_sender", -1, -1, e.getClass().getSimpleName(), workerIndex);
|
||||
lastUplinkSendErrorMessage = compactException(e);
|
||||
writeRuntimeDetail("retry", "uplink send retry worker=" + workerIndex + " relay=" + relayUrl + " attempt=" + attempt + " error=" + lastUplinkSendErrorMessage, "uplink_sender", -1, -1, e.getClass().getSimpleName(), workerIndex);
|
||||
sleepQuietly(UPLINK_SEND_RETRY_SLEEP_MS * (attempt + 1L));
|
||||
}
|
||||
}
|
||||
@@ -2323,6 +2496,9 @@ public class RapVpnService extends VpnService {
|
||||
}
|
||||
|
||||
private boolean sendUplinkBatchOverWebSocket(String relayUrl, String clusterId, String vpnConnectionId, List<byte[]> batch, int workerIndex) {
|
||||
if (!PACKET_WEBSOCKET_DATAPLANE_ENABLED) {
|
||||
return false;
|
||||
}
|
||||
VpnPacketWebSocketRelay relay = packetWebSocketRelay;
|
||||
if (relay == null || relayUrl == null || !relayUrl.equals(relay.baseUrl())) {
|
||||
return false;
|
||||
@@ -2555,6 +2731,7 @@ public class RapVpnService extends VpnService {
|
||||
}
|
||||
addDefaultDnsProbeDomains(domains);
|
||||
int resolved = 0;
|
||||
int tcpOk = 0;
|
||||
String last = "";
|
||||
long warmUntil = System.currentTimeMillis() + 30000;
|
||||
int pass = 0;
|
||||
@@ -2579,7 +2756,14 @@ public class RapVpnService extends VpnService {
|
||||
}
|
||||
sleepQuietly(120);
|
||||
}
|
||||
if (passResolved >= Math.min(3, domains.size()) && pass >= 2) {
|
||||
if (pass <= VPN_TCP_WARMUP_PASSES) {
|
||||
int passTcpOk = runTCPWarmupPass(vpn);
|
||||
tcpOk += passTcpOk;
|
||||
if (passTcpOk > 0) {
|
||||
last = "tcp_warmup_ok=" + passTcpOk;
|
||||
}
|
||||
}
|
||||
if (passResolved >= Math.min(3, domains.size()) && tcpOk > 0 && pass >= 2) {
|
||||
break;
|
||||
}
|
||||
sleepQuietly(750);
|
||||
@@ -2588,15 +2772,60 @@ public class RapVpnService extends VpnService {
|
||||
return;
|
||||
}
|
||||
if (resolved > 0) {
|
||||
writeRuntimeStatus("ready", "vpn ready; dns warmup ok " + resolved + " " + dnsInfo + " " + last, 0, 0, downlinkReceivedPackets.get(), 0);
|
||||
writeRuntimeStatus("ready", "vpn ready; warmup dns=" + resolved + " tcp=" + tcpOk + " " + dnsInfo + " " + last, 0, 0, downlinkReceivedPackets.get(), 0);
|
||||
} else {
|
||||
writeRuntimeStatus("warming", "vpn started; dns warmup pending " + dnsInfo + " " + last, 0, 0, downlinkReceivedPackets.get(), 1);
|
||||
writeRuntimeStatus("warming", "vpn started; warmup pending dns=0 tcp=" + tcpOk + " " + dnsInfo + " " + last, 0, 0, downlinkReceivedPackets.get(), 1);
|
||||
}
|
||||
Log.i(TAG, "vpn readiness warmup complete: connection=" + vpnConnectionId + " resolved=" + resolved + " " + dnsInfo + " " + last);
|
||||
Log.i(TAG, "vpn readiness warmup complete: connection=" + vpnConnectionId + " resolved=" + resolved + " tcp=" + tcpOk + " " + dnsInfo + " " + last);
|
||||
}, "rap-vpn-readiness-warmup");
|
||||
thread.start();
|
||||
}
|
||||
|
||||
private int runTCPWarmupPass(Network vpn) {
|
||||
int ok = 0;
|
||||
for (String target : DEFAULT_TCP_WARMUP_TARGETS) {
|
||||
if (!running) {
|
||||
return ok;
|
||||
}
|
||||
TCPWarmupTarget parsed = parseTCPWarmupTarget(target);
|
||||
if (parsed == null) {
|
||||
continue;
|
||||
}
|
||||
long started = System.currentTimeMillis();
|
||||
try (Socket socket = new Socket()) {
|
||||
if (vpn != null) {
|
||||
vpn.bindSocket(socket);
|
||||
}
|
||||
socket.connect(new InetSocketAddress(parsed.host, parsed.port), VPN_TCP_WARMUP_CONNECT_TIMEOUT_MS);
|
||||
ok++;
|
||||
writeRuntimeDetail("tcp_warmup", target + " ms=" + (System.currentTimeMillis() - started), "readiness", ok, 0, "", -1);
|
||||
} catch (Exception e) {
|
||||
writeRuntimeDetail("tcp_warmup_failed", target + " " + e.getClass().getSimpleName(), "readiness", ok, 1, e.getClass().getSimpleName(), -1);
|
||||
}
|
||||
sleepQuietly(120);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private TCPWarmupTarget parseTCPWarmupTarget(String target) {
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
int split = target.lastIndexOf(':');
|
||||
if (split <= 0 || split >= target.length() - 1) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
int port = Integer.parseInt(target.substring(split + 1));
|
||||
if (port <= 0 || port > 65535) {
|
||||
return null;
|
||||
}
|
||||
return new TCPWarmupTarget(target.substring(0, split), port);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void addDefaultDnsProbeDomains(Set<String> domains) {
|
||||
if (domains == null) {
|
||||
return;
|
||||
@@ -2724,6 +2953,9 @@ public class RapVpnService extends VpnService {
|
||||
}
|
||||
|
||||
private List<byte[]> receiveDownlinkBatch(String relayUrl, RapApiClient client, String clusterId, String vpnConnectionId, int timeoutMs) throws Exception {
|
||||
if (!PACKET_WEBSOCKET_DATAPLANE_ENABLED) {
|
||||
return client.receiveClientPacketBatch(clusterId, vpnConnectionId, timeoutMs);
|
||||
}
|
||||
VpnPacketWebSocketRelay relay = packetWebSocketRelay;
|
||||
if (relay != null && relayUrl != null && relayUrl.equals(relay.baseUrl())) {
|
||||
List<byte[]> packets = relay.receiveClientPacketBatch(clusterId, vpnConnectionId, timeoutMs);
|
||||
@@ -2890,6 +3122,16 @@ public class RapVpnService extends VpnService {
|
||||
}
|
||||
}
|
||||
|
||||
private static class TCPWarmupTarget {
|
||||
final String host;
|
||||
final int port;
|
||||
|
||||
TCPWarmupTarget(String host, int port) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean writePacketToTun(FileDescriptor fd, byte[] packet, int packetLength) throws Exception {
|
||||
int offset = 0;
|
||||
int attempts = 0;
|
||||
@@ -2950,6 +3192,19 @@ public class RapVpnService extends VpnService {
|
||||
}
|
||||
}
|
||||
|
||||
private String compactException(Exception e) {
|
||||
if (e == null) {
|
||||
return "";
|
||||
}
|
||||
String message = e.getMessage();
|
||||
String value = e.getClass().getSimpleName() + (message == null || message.trim().isEmpty() ? "" : ": " + message.trim());
|
||||
value = value.replace('\n', ' ').replace('\r', ' ').trim();
|
||||
if (value.length() > 240) {
|
||||
return value.substring(0, 240);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private void closeFdQuietly(FileDescriptor fd) {
|
||||
if (fd == null) {
|
||||
return;
|
||||
@@ -3064,6 +3319,8 @@ public class RapVpnService extends VpnService {
|
||||
.putLong("runtime_watchdog_recoveries", runtimeWatchdogRecoveries.get())
|
||||
.putLong("tcp_handshake_stalls", tcpHandshakeStalls.get())
|
||||
.putLong("runtime_watchdog_hard_restarts", runtimeWatchdogHardRestarts.get())
|
||||
.putLong("diagnostic_watchdog_ensure_requests", diagnosticEnsureRequests.get())
|
||||
.putLong("diagnostic_watchdog_restart_requests", diagnosticRestartRequests.get())
|
||||
.putLong("uplink_source_mismatch_packets", uplinkSourceMismatchPackets.get())
|
||||
.putLong("downlink_destination_mismatch_packets", downlinkDestinationMismatchPackets.get())
|
||||
.putFloat("uplink_read_mbps", uplinkReadMbps)
|
||||
|
||||
Reference in New Issue
Block a user