рабочий вариант, но скороть 10 МБит
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.
@@ -17,19 +17,18 @@ Implemented now:
|
||||
- run as a normal fabric node with the `vpn-client` service role. The local
|
||||
`VpnService` TUN is the IPv4 ingress for that node, and packet channels are
|
||||
routed by the farm to an authorized `ipv4-egress` pool. The supported
|
||||
dataplane is the QUIC fabric runtime only. HTTP batch forwarding, WebSocket
|
||||
packet relay, direct backend packet relay, and old VPN protocols are removed
|
||||
from the runtime path.
|
||||
- user-facing HOME-first screen: connect/disconnect is primary, while backend,
|
||||
cluster, organization, login, and password are kept in the settings dialog;
|
||||
dataplane is the QUIC fabric runtime only. The Android node never contacts
|
||||
the farm over HTTP; control, profile refresh, channel request, packet uplink,
|
||||
and downlink all go through the farm protocol.
|
||||
- user-facing HOME-first screen: connect/disconnect is primary, while cluster,
|
||||
organization, login, and password are kept in the settings dialog;
|
||||
- saved connection settings in app preferences so repeat connects do not require
|
||||
retyping the profile.
|
||||
- encrypted refresh-token storage through Android Keystore. If the trusted
|
||||
device session is revoked or expires, the app asks for the password once and
|
||||
then rotates the device keys/profile again.
|
||||
- no separate diagnostic foreground service: runtime status is reported by the
|
||||
node/VPN service itself, so the Android build does not keep a parallel legacy
|
||||
control process alive.
|
||||
node/VPN service itself.
|
||||
|
||||
This is still a lab runtime. The required target model is Android as a farm
|
||||
node with the `vpn-client` role. The VPN service must attach to the mesh as
|
||||
|
||||
@@ -22,12 +22,10 @@ android {
|
||||
return (value == null ? "" : value.toString()).replace("\\", "\\\\").replace("\"", "\\\"")
|
||||
}
|
||||
|
||||
def defaultBackendUrl = project.findProperty("RAP_ANDROID_DEFAULT_BACKEND_URL") ?: ""
|
||||
// This is a node bootstrap seed set, not an API/backend selector. The
|
||||
// Android app installs as a fabric node and tries every QUIC endpoint that
|
||||
// may be reachable from its current network: public nodes, LAN nodes, or a
|
||||
// closed-site neighbor that can route onward through the fabric.
|
||||
def defaultFabricBootstrapPeers = project.findProperty("RAP_ANDROID_FABRIC_BOOTSTRAP_PEERS") ?: "quic://94.141.118.222:19199#sha256=49892029a27db9c394a41bc4cb917d9cceb1f86219417c351764d2ed9d6bc683,quic://94.141.118.222:19191#sha256=72e51f1631b32c3a7d1e8732fe3325e0395a897a5aa31db645888c142e4ae401,quic://192.168.200.61:19134#sha256=72e51f1631b32c3a7d1e8732fe3325e0395a897a5aa31db645888c142e4ae401,quic://192.168.200.61:19132#sha256=8d28b75144d25d29e3b8f8022b6165258ce3cb0e227a2d9d97996839abb89c2a,quic://192.168.200.61:19133#sha256=a71b07e55b810f57b01696c485b765b336983e963238163085824bf04022ecaa,quic://192.168.200.85:18080#sha256=49892029a27db9c394a41bc4cb917d9cceb1f86219417c351764d2ed9d6bc683,quic://192.168.200.85:18081#sha256=2a3be67e6345943a36cfa1197a5879c2b112c81adc019fd1ee9d7dffbf188b57,quic://192.168.200.85:18082#sha256=a318c1a756ff43595635961768dfd1677afa7e2cbf945d724c107ff82426378a"
|
||||
// This is a node bootstrap seed set, not an API/backend selector. Android
|
||||
// runs the same fabric node core as Linux/Windows; only its local IPv4
|
||||
// ingress adapter is Android VpnService/TUN.
|
||||
def defaultFabricBootstrapPeers = project.findProperty("RAP_ANDROID_FABRIC_BOOTSTRAP_PEERS") ?: "quic://94.141.118.222:19199,quic://94.141.118.222:19191,quic://195.123.240.88:19131,quic://192.168.200.61:19134,quic://192.168.200.61:19135,quic://192.168.200.61:19136,quic://192.168.200.61:19137"
|
||||
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"
|
||||
|
||||
@@ -35,9 +33,8 @@ android {
|
||||
applicationId "su.cin.rapvpn"
|
||||
minSdk 26
|
||||
targetSdk 35
|
||||
versionCode 239
|
||||
versionName "0.2.239"
|
||||
buildConfigField "String", "DEFAULT_BACKEND_URL", "\"${normalizeGradleString(defaultBackendUrl)}\""
|
||||
versionCode 270
|
||||
versionName "0.2.270"
|
||||
buildConfigField "String", "FABRIC_BOOTSTRAP_PEERS", "\"${normalizeGradleString(defaultFabricBootstrapPeers)}\""
|
||||
buildConfigField "String", "DEFAULT_CLUSTER_ID", "\"${normalizeGradleString(defaultClusterId)}\""
|
||||
buildConfigField "String", "DEFAULT_ORGANIZATION_ID", "\"${normalizeGradleString(defaultOrganizationId)}\""
|
||||
@@ -52,5 +49,4 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation files("libs/rap-fabricvpn.aar")
|
||||
implementation "com.squareup.okhttp3:okhttp:5.3.2"
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -36,7 +36,7 @@ public class MainActivity extends Activity {
|
||||
private static final String PREF_USER_ID = "user_id";
|
||||
private static final String PREF_DEVICE_ID = "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 String PREF_TUNNEL_ID = "tunnel_id";
|
||||
static final String PREF_FORCE_FULL_TUNNEL = "force_full_tunnel";
|
||||
private EditText clusterId;
|
||||
private EditText organizationId;
|
||||
@@ -74,7 +74,14 @@ public class MainActivity extends Activity {
|
||||
prefs.edit().putBoolean(PREF_FORCE_FULL_TUNNEL, true).apply();
|
||||
}
|
||||
profileJson = prefs.getString(PREF_PROFILE_JSON, "");
|
||||
vpnConnectionId = prefs.getString(PREF_VPN_CONNECTION_ID, "");
|
||||
vpnConnectionId = prefs.getString(PREF_TUNNEL_ID, "");
|
||||
if (vpnConnectionId.isEmpty() && !profileJson.trim().isEmpty()) {
|
||||
try {
|
||||
vpnConnectionId = firstConnectionId(profileJson);
|
||||
prefs.edit().putString(PREF_TUNNEL_ID, vpnConnectionId).apply();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
restoreAuthContext();
|
||||
|
||||
TextView title = new TextView(this);
|
||||
@@ -154,6 +161,7 @@ public class MainActivity extends Activity {
|
||||
setContentView(root);
|
||||
scheduleRuntimeStatusRefresh();
|
||||
registerCandidateNodeAsync(false);
|
||||
autoResumeVpnIfReady();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -225,7 +233,7 @@ public class MainActivity extends Activity {
|
||||
intent.putExtra(RapVpnService.EXTRA_PROFILE_JSON, profileJson);
|
||||
intent.putExtra(RapVpnService.EXTRA_FABRIC_BOOTSTRAP_CONFIG, fabricControlConfig());
|
||||
intent.putExtra(RapVpnService.EXTRA_CLUSTER_ID, clusterId.getText().toString());
|
||||
intent.putExtra(RapVpnService.EXTRA_VPN_CONNECTION_ID, vpnConnectionId);
|
||||
intent.putExtra(RapVpnService.EXTRA_TUNNEL_ID, vpnConnectionId);
|
||||
startForegroundService(intent);
|
||||
status.setText("VPN подключается через ферму. Версия " + APP_VERSION + ". Ожидаю рабочий канал.");
|
||||
runtimeStatus.setText("Запрашиваю статус... " + runtimeStatusText());
|
||||
@@ -253,6 +261,18 @@ public class MainActivity extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
private void autoResumeVpnIfReady() {
|
||||
if (prefs.getBoolean("manual_stopped", false) || !hasSelectedPool() || isVpnRuntimeActive()) {
|
||||
return;
|
||||
}
|
||||
if (VpnService.prepare(this) != null) {
|
||||
status.setText("Профиль готов. Нажмите «Подключить», чтобы Android выдал разрешение VPN.");
|
||||
return;
|
||||
}
|
||||
status.setText("Профиль готов. Автоматически восстанавливаю VPN через ферму.");
|
||||
runtimeStatus.postDelayed(this::startVpn, 500);
|
||||
}
|
||||
|
||||
private void scheduleRuntimeStatusRefresh() {
|
||||
runtimeStatus.postDelayed(() -> {
|
||||
runtimeStatus.setText(runtimeStatusText());
|
||||
@@ -414,7 +434,7 @@ public class MainActivity extends Activity {
|
||||
}
|
||||
|
||||
private String firstConnectionId(String profile) throws Exception {
|
||||
String selected = prefs == null ? "" : prefs.getString(PREF_VPN_CONNECTION_ID, "").trim();
|
||||
String selected = prefs == null ? "" : prefs.getString(PREF_TUNNEL_ID, "").trim();
|
||||
if (!selected.isEmpty() && profileContainsConnection(profile, selected)) {
|
||||
return selected;
|
||||
}
|
||||
@@ -448,9 +468,16 @@ public class MainActivity extends Activity {
|
||||
}
|
||||
String status = fabricRoute.optString("status", "").trim().toLowerCase();
|
||||
if ("planned".equals(status)) {
|
||||
String entry = fabricRoute.optString("selected_entry_node_id", "").trim();
|
||||
String exit = fabricRoute.optString("selected_exit_node_id", "").trim();
|
||||
if (!exit.isEmpty()) {
|
||||
JSONObject dataplaneSession = clientConfig.optJSONObject("vpn_dataplane_session");
|
||||
JSONObject tunnel = dataplaneSession == null ? null : dataplaneSession.optJSONObject("fabric_service_tunnel");
|
||||
String pool = firstNonEmpty(
|
||||
dataplaneSession == null ? "" : dataplaneSession.optString("pool_id", ""),
|
||||
tunnel == null ? "" : tunnel.optString("pool_id", ""),
|
||||
fabricRoute.optString("exit_pool_id", "")).trim();
|
||||
String tunnelId = firstNonEmpty(
|
||||
dataplaneSession == null ? "" : dataplaneSession.optString("tunnel_id", ""),
|
||||
tunnel == null ? "" : tunnel.optString("tunnel_id", "")).trim();
|
||||
if (!pool.isEmpty() || !tunnelId.isEmpty()) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -504,7 +531,7 @@ public class MainActivity extends Activity {
|
||||
private String summaryText() {
|
||||
String deviceId = prefs == null ? "" : prefs.getString(PREF_DEVICE_ID, "");
|
||||
String connectionId = vpnConnectionId == null || vpnConnectionId.isEmpty()
|
||||
? (prefs == null ? "" : prefs.getString(PREF_VPN_CONNECTION_ID, ""))
|
||||
? (prefs == null ? "" : prefs.getString(PREF_TUNNEL_ID, ""))
|
||||
: vpnConnectionId;
|
||||
String poolText = availablePoolsText();
|
||||
String selectedPoolText = selectedPoolName();
|
||||
@@ -512,8 +539,8 @@ public class MainActivity extends Activity {
|
||||
return "Версия: " + APP_VERSION
|
||||
+ "\nУзел Android: в ферме"
|
||||
+ "\nBootstrap фермы: " + bootstrapPeerCount() + " узл."
|
||||
+ "\nДоступные выходы: " + (poolText.isEmpty() ? "не загружены" : poolText)
|
||||
+ "\nВыбранный выход: " + (selectedPoolText.isEmpty() ? "не выбран" : selectedPoolText)
|
||||
+ "\nДоступные пулы: " + (poolText.isEmpty() ? "не загружены" : poolText)
|
||||
+ "\nВыбранный пул: " + (selectedPoolText.isEmpty() ? "не выбран" : selectedPoolText)
|
||||
+ "\nDNS выхода: " + (profileDNS.isEmpty() ? "будет получен из профиля" : profileDNS)
|
||||
+ "\nТрафик: " + (prefs.getBoolean(PREF_FORCE_FULL_TUNNEL, true) ? "весь через VPN" : "по профилю")
|
||||
+ "\nDevice: " + (deviceId.isEmpty() ? "нет" : deviceId)
|
||||
@@ -580,7 +607,7 @@ public class MainActivity extends Activity {
|
||||
return "";
|
||||
}
|
||||
String preferredConnection = vpnConnectionId == null || vpnConnectionId.isEmpty()
|
||||
? (prefs == null ? "" : prefs.getString(PREF_VPN_CONNECTION_ID, ""))
|
||||
? (prefs == null ? "" : prefs.getString(PREF_TUNNEL_ID, ""))
|
||||
: vpnConnectionId;
|
||||
for (int i = 0; i < connections.length(); i++) {
|
||||
JSONObject connection = connections.optJSONObject(i);
|
||||
@@ -613,7 +640,7 @@ public class MainActivity extends Activity {
|
||||
return "";
|
||||
}
|
||||
String preferredConnection = vpnConnectionId == null || vpnConnectionId.isEmpty()
|
||||
? (prefs == null ? "" : prefs.getString(PREF_VPN_CONNECTION_ID, ""))
|
||||
? (prefs == null ? "" : prefs.getString(PREF_TUNNEL_ID, ""))
|
||||
: vpnConnectionId;
|
||||
JSONObject selected = null;
|
||||
for (int i = 0; i < connections.length(); i++) {
|
||||
@@ -712,11 +739,11 @@ public class MainActivity extends Activity {
|
||||
service.put("schema_version", "rap.fabric_service_channel_request.v1");
|
||||
service.put("channel_id", "android-control");
|
||||
service.put("service_class", "identity_runtime");
|
||||
service.put("source_role", "vpn-client");
|
||||
service.put("source_role", "ipv4-ingress");
|
||||
JSONObject cfg = new JSONObject();
|
||||
cfg.put("cluster_id", DEFAULT_CLUSTER_ID);
|
||||
cfg.put("local_node_id", fabricNodeId());
|
||||
cfg.put("vpn_connection_id", "fabric-control");
|
||||
cfg.put("tunnel_id", "fabric-control");
|
||||
cfg.put("stream_shards", 1);
|
||||
cfg.put("service_channel_request", service);
|
||||
cfg.put("endpoints", endpoints);
|
||||
@@ -822,14 +849,14 @@ public class MainActivity extends Activity {
|
||||
.putString(PREF_DEVICE_ID, context.deviceId)
|
||||
.putString(PREF_REFRESH_EXPIRES_AT, context.refreshTokenExpiresAt)
|
||||
.putString(PREF_PROFILE_JSON, profileJson)
|
||||
.putString(PREF_VPN_CONNECTION_ID, vpnConnectionId)
|
||||
.putString(PREF_TUNNEL_ID, vpnConnectionId)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private void saveProfileState() {
|
||||
prefs.edit()
|
||||
.putString(PREF_PROFILE_JSON, profileJson)
|
||||
.putString(PREF_VPN_CONNECTION_ID, vpnConnectionId)
|
||||
.putString(PREF_TUNNEL_ID, vpnConnectionId)
|
||||
.apply();
|
||||
}
|
||||
|
||||
@@ -854,7 +881,7 @@ public class MainActivity extends Activity {
|
||||
.remove(PREF_USER_ID)
|
||||
.remove(PREF_DEVICE_ID);
|
||||
if (clearProfile) {
|
||||
editor.remove(PREF_PROFILE_JSON).remove(PREF_VPN_CONNECTION_ID);
|
||||
editor.remove(PREF_PROFILE_JSON).remove(PREF_TUNNEL_ID);
|
||||
profileJson = "";
|
||||
vpnConnectionId = "";
|
||||
}
|
||||
@@ -867,15 +894,15 @@ public class MainActivity extends Activity {
|
||||
if (!token.isEmpty()) {
|
||||
return token;
|
||||
}
|
||||
String legacyToken = prefs.getString(PREF_REFRESH_TOKEN, "");
|
||||
if (!legacyToken.isEmpty()) {
|
||||
String plaintextToken = prefs.getString(PREF_REFRESH_TOKEN, "");
|
||||
if (!plaintextToken.isEmpty()) {
|
||||
try {
|
||||
secureTokens.put(PREF_REFRESH_TOKEN, legacyToken);
|
||||
secureTokens.put(PREF_REFRESH_TOKEN, plaintextToken);
|
||||
prefs.edit().remove(PREF_REFRESH_TOKEN).apply();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return legacyToken;
|
||||
return plaintextToken;
|
||||
}
|
||||
|
||||
private String deviceFingerprint() {
|
||||
@@ -918,7 +945,7 @@ public class MainActivity extends Activity {
|
||||
private String registerCandidateNode(RapApiClient client) throws Exception {
|
||||
String nodeId = fabricNodeId();
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("source", "android_vpn_client");
|
||||
metadata.put("source", "ipv4_ingress_adapter");
|
||||
metadata.put("candidate_access", true);
|
||||
metadata.put("fabric_transport", "quic");
|
||||
metadata.put("connectivity_mode", "outbound_only");
|
||||
@@ -926,8 +953,8 @@ public class MainActivity extends Activity {
|
||||
metadata.put("device_fingerprint", deviceFingerprint());
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("cluster_id", clusterId.getText().toString().trim().isEmpty() ? DEFAULT_CLUSTER_ID : clusterId.getText().toString().trim());
|
||||
payload.put("node_key", "android-vpn:" + deviceFingerprint());
|
||||
payload.put("name", "android-vpn-" + deviceFingerprint().replace("android-", "").substring(0, Math.min(8, deviceFingerprint().replace("android-", "").length())));
|
||||
payload.put("node_key", "ipv4-ingress:" + deviceFingerprint());
|
||||
payload.put("name", "ipv4-ingress-" + deviceFingerprint().replace("android-", "").substring(0, Math.min(8, deviceFingerprint().replace("android-", "").length())));
|
||||
payload.put("ownership_type", "customer_managed");
|
||||
payload.put("owner_organization_id", organizationId.getText().toString().trim().isEmpty() ? DEFAULT_ORGANIZATION_ID : organizationId.getText().toString().trim());
|
||||
payload.put("reported_version", APP_VERSION);
|
||||
@@ -944,19 +971,23 @@ public class MainActivity extends Activity {
|
||||
private void sendCandidateHeartbeat(RapApiClient client, String nodeId) throws Exception {
|
||||
JSONObject capabilities = new JSONObject();
|
||||
capabilities.put("fabric_quic_node", true);
|
||||
capabilities.put("android_vpn_client", true);
|
||||
capabilities.put("ipv4_ingress", true);
|
||||
capabilities.put("candidate_access", true);
|
||||
capabilities.put("vpn_client", true);
|
||||
capabilities.put("local_packet_io", true);
|
||||
JSONObject serviceStates = new JSONObject();
|
||||
serviceStates.put("vpn-client", new JSONObject()
|
||||
.put("state", isSystemVpnActive() ? "running" : "candidate")
|
||||
JSONObject runtime = runtimeSnapshot();
|
||||
String runtimeState = runtime.optString("state", "");
|
||||
serviceStates.put("ipv4-ingress", new JSONObject()
|
||||
.put("state", isSystemVpnActive() ? firstNonEmpty(runtimeState, "running") : "candidate")
|
||||
.put("runtime", "android_vpnservice")
|
||||
.put("transport", "fabric_quic_route"));
|
||||
.put("transport", "fabric_quic_route")
|
||||
.put("status", runtime));
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put("source", "android_vpn_client");
|
||||
metadata.put("source", "ipv4_ingress_adapter");
|
||||
metadata.put("candidate", true);
|
||||
metadata.put("passive", true);
|
||||
metadata.put("app_version", APP_VERSION);
|
||||
metadata.put("vpn_runtime_status", runtime);
|
||||
metadata.put("mesh_endpoint_report", new JSONObject()
|
||||
.put("schema_version", "rap.mesh_endpoint_report.v1")
|
||||
.put("transport", "quic")
|
||||
@@ -971,6 +1002,42 @@ public class MainActivity extends Activity {
|
||||
client.sendFabricNodeHeartbeat(clusterId.getText().toString().trim().isEmpty() ? DEFAULT_CLUSTER_ID : clusterId.getText().toString().trim(), nodeId, payload);
|
||||
}
|
||||
|
||||
private JSONObject runtimeSnapshot() {
|
||||
JSONObject runtime = new JSONObject();
|
||||
try {
|
||||
runtime.put("schema_version", "rap.android_vpn_runtime_status.v1");
|
||||
runtime.put("state", runtimePrefs.getString("state", ""));
|
||||
runtime.put("message", runtimePrefs.getString("message", ""));
|
||||
runtime.put("updated_at_ms", runtimePrefs.getLong("updated_at", 0));
|
||||
runtime.put("runtime_started_at_ms", runtimePrefs.getLong("runtime_started_at", 0));
|
||||
runtime.put("selected_connection_id", prefs.getString(PREF_TUNNEL_ID, ""));
|
||||
runtime.put("system_vpn_active", isSystemVpnActive());
|
||||
runtime.put("uplink_read_packets", runtimePrefs.getLong("uplink_read_total", 0));
|
||||
runtime.put("uplink_sent_packets", runtimePrefs.getLong("uplink_sent_total", 0));
|
||||
runtime.put("downlink_received_packets", runtimePrefs.getLong("downlink_received_total", 0));
|
||||
runtime.put("uplink_dropped_packets", runtimePrefs.getLong("uplink_dropped_packets", 0));
|
||||
runtime.put("downlink_dropped_packets", runtimePrefs.getLong("downlink_dropped_packets", 0));
|
||||
runtime.put("local_dns_queries", runtimePrefs.getLong("local_dns_queries", 0));
|
||||
runtime.put("local_dns_replies", runtimePrefs.getLong("local_dns_replies", 0));
|
||||
runtime.put("local_dns_errors", runtimePrefs.getLong("local_dns_errors", 0));
|
||||
runtime.put("runtime_watchdog_recoveries", runtimePrefs.getLong("runtime_watchdog_recoveries", 0));
|
||||
runtime.put("runtime_watchdog_hard_restarts", runtimePrefs.getLong("runtime_watchdog_hard_restarts", 0));
|
||||
runtime.put("uplink_read_mbps", runtimePrefs.getFloat("uplink_read_mbps", 0));
|
||||
runtime.put("uplink_sent_mbps", runtimePrefs.getFloat("uplink_sent_mbps", 0));
|
||||
runtime.put("downlink_received_mbps", runtimePrefs.getFloat("downlink_received_mbps", 0));
|
||||
runtime.put("fabric_state", runtimePrefs.getString("fabric_state", ""));
|
||||
runtime.put("fabric_message", runtimePrefs.getString("fabric_message", ""));
|
||||
runtime.put("uplink_state", runtimePrefs.getString("uplink_state", ""));
|
||||
runtime.put("uplink_message", runtimePrefs.getString("uplink_message", ""));
|
||||
runtime.put("downlink_state", runtimePrefs.getString("downlink_state", ""));
|
||||
runtime.put("downlink_message", runtimePrefs.getString("downlink_message", ""));
|
||||
runtime.put("downlink_writer_state", runtimePrefs.getString("downlink_writer_state", ""));
|
||||
runtime.put("downlink_writer_message", runtimePrefs.getString("downlink_writer_message", ""));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return runtime;
|
||||
}
|
||||
|
||||
private void showSettingsDialog() {
|
||||
LinearLayout form = new LinearLayout(this);
|
||||
form.setOrientation(LinearLayout.VERTICAL);
|
||||
@@ -1000,7 +1067,7 @@ public class MainActivity extends Activity {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Настройка VPN")
|
||||
.setView(form)
|
||||
.setPositiveButton("Войти и выбрать выход", (dialog, which) -> {
|
||||
.setPositiveButton("Войти и выбрать пул", (dialog, which) -> {
|
||||
email.setText(emailDraft.getText().toString());
|
||||
String passwordValue = passwordDraft.getText().toString();
|
||||
password.setText("");
|
||||
@@ -1017,7 +1084,7 @@ public class MainActivity extends Activity {
|
||||
}
|
||||
|
||||
private void loginAndChoosePool(String emailValue, String passwordValue) {
|
||||
status.setText("Идентифицирую устройство и загружаю доступные выходы...");
|
||||
status.setText("Идентифицирую устройство и загружаю доступные пулы...");
|
||||
new Thread(() -> {
|
||||
try {
|
||||
RapApiClient client = new RapApiClient(fabricControlConfig(), this);
|
||||
@@ -1082,6 +1149,18 @@ public class MainActivity extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
private String firstNonEmpty(String... values) {
|
||||
if (values == null) {
|
||||
return "";
|
||||
}
|
||||
for (String value : values) {
|
||||
if (value != null && !value.trim().isEmpty()) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String friendlyError(Exception ex) {
|
||||
String message = ex.getMessage();
|
||||
if (message == null || message.trim().isEmpty()) {
|
||||
|
||||
@@ -1,41 +1,21 @@
|
||||
package su.cin.rapvpn;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Dispatcher;
|
||||
import okhttp3.ConnectionPool;
|
||||
import okhttp3.Dns;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import android.net.VpnService;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import su.cin.rapvpn.fabric.fabricvpn.Fabricvpn;
|
||||
import su.cin.rapvpn.fabric.fabricvpn.Manager;
|
||||
import su.cin.rapvpn.fabric.fabricvpn.SocketProtector;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
final class RapApiClient {
|
||||
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
|
||||
private final String baseUrl;
|
||||
private final OkHttpClient httpClient;
|
||||
private final String networkMode;
|
||||
private final Context context;
|
||||
private final Manager fabricControlManager;
|
||||
|
||||
RapApiClient(String baseUrl) {
|
||||
@@ -44,65 +24,43 @@ final class RapApiClient {
|
||||
|
||||
RapApiClient(String baseUrl, Context context) {
|
||||
this.baseUrl = trimRight(baseUrl);
|
||||
this.context = context;
|
||||
this.fabricControlManager = startFabricControlManager(baseUrl);
|
||||
OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
||||
// Regular app and diagnostic requests should use Android's default
|
||||
// routing. Some devices reject binding app sockets to a specific
|
||||
// Network with EACCES, which must not block login/profile refresh.
|
||||
this.networkMode = context == null ? "default_network" : "default_network_context";
|
||||
builder.dns(new BackendPinnedDns(baseUrl));
|
||||
builder.connectTimeout(5, TimeUnit.SECONDS);
|
||||
builder.writeTimeout(12, TimeUnit.SECONDS);
|
||||
builder.readTimeout(12, TimeUnit.SECONDS);
|
||||
builder.callTimeout(15, TimeUnit.SECONDS);
|
||||
builder.retryOnConnectionFailure(true);
|
||||
Dispatcher dispatcher = new Dispatcher();
|
||||
dispatcher.setMaxRequests(64);
|
||||
dispatcher.setMaxRequestsPerHost(32);
|
||||
builder.dispatcher(dispatcher);
|
||||
builder.connectionPool(new ConnectionPool(16, 5, TimeUnit.MINUTES));
|
||||
this.httpClient = builder.build();
|
||||
this.networkMode = context == null ? "fabric_control" : "fabric_control_context";
|
||||
}
|
||||
|
||||
RapApiClient(String baseUrl, Context context, boolean preferUnderlyingNetwork) {
|
||||
this.baseUrl = trimRight(baseUrl);
|
||||
this.fabricControlManager = startFabricControlManager(baseUrl);
|
||||
OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
||||
String mode = context == null ? "default_network" : "default_network_context";
|
||||
if (preferUnderlyingNetwork && context != null) {
|
||||
SocketFactory socketFactory = underlyingSocketFactory(context);
|
||||
if (socketFactory != null) {
|
||||
builder.socketFactory(socketFactory);
|
||||
mode = "underlying_network_context";
|
||||
}
|
||||
}
|
||||
this.networkMode = mode;
|
||||
builder.dns(new BackendPinnedDns(baseUrl));
|
||||
builder.connectTimeout(3, TimeUnit.SECONDS);
|
||||
builder.writeTimeout(6, TimeUnit.SECONDS);
|
||||
builder.readTimeout(6, TimeUnit.SECONDS);
|
||||
builder.callTimeout(8, TimeUnit.SECONDS);
|
||||
builder.retryOnConnectionFailure(true);
|
||||
Dispatcher dispatcher = new Dispatcher();
|
||||
dispatcher.setMaxRequests(64);
|
||||
dispatcher.setMaxRequestsPerHost(32);
|
||||
builder.dispatcher(dispatcher);
|
||||
builder.connectionPool(new ConnectionPool(16, 5, TimeUnit.MINUTES));
|
||||
this.httpClient = builder.build();
|
||||
this(baseUrl, context);
|
||||
}
|
||||
|
||||
String networkMode() {
|
||||
return networkMode;
|
||||
}
|
||||
|
||||
void close() {
|
||||
try {
|
||||
fabricControlManager.stop();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private Manager startFabricControlManager(String config) {
|
||||
String value = config == null ? "" : config.trim();
|
||||
if (!value.startsWith("{")) {
|
||||
return null;
|
||||
throw new IllegalStateException("Android узел должен подключаться к ферме только через QUIC fabric bootstrap.");
|
||||
}
|
||||
try {
|
||||
Fabricvpn.touch();
|
||||
Manager manager = Fabricvpn.newManager();
|
||||
if (context instanceof VpnService) {
|
||||
VpnService vpnService = (VpnService) context;
|
||||
manager.setSocketProtector(new SocketProtector() {
|
||||
@Override
|
||||
public boolean protect(long fd) {
|
||||
return vpnService.protect((int) fd);
|
||||
}
|
||||
});
|
||||
}
|
||||
manager.start(value);
|
||||
return manager;
|
||||
} catch (Exception e) {
|
||||
@@ -111,45 +69,6 @@ final class RapApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
static final class BackendPinnedDns implements Dns {
|
||||
private final String backendHost;
|
||||
|
||||
BackendPinnedDns(String baseUrl) {
|
||||
String parsedHost = "";
|
||||
try {
|
||||
parsedHost = URI.create(baseUrl == null ? "" : baseUrl).getHost();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
backendHost = parsedHost == null ? "" : parsedHost.trim().toLowerCase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
|
||||
return Dns.SYSTEM.lookup(hostname);
|
||||
}
|
||||
}
|
||||
|
||||
private SocketFactory underlyingSocketFactory(Context context) {
|
||||
ConnectivityManager connectivity = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (connectivity == null) {
|
||||
return null;
|
||||
}
|
||||
for (Network network : connectivity.getAllNetworks()) {
|
||||
NetworkCapabilities capabilities = connectivity.getNetworkCapabilities(network);
|
||||
if (capabilities == null) {
|
||||
continue;
|
||||
}
|
||||
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
|
||||
continue;
|
||||
}
|
||||
if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
|
||||
continue;
|
||||
}
|
||||
return network.getSocketFactory();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
AuthContext login(String email, String password, String deviceFingerprint) throws Exception {
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("email", email);
|
||||
@@ -167,11 +86,8 @@ final class RapApiClient {
|
||||
return parseAuthContext(post("/auth/refresh", body));
|
||||
}
|
||||
|
||||
String vpnClientProfile(String clusterId, String organizationId, String userId, String exitNodeId) throws Exception {
|
||||
String vpnClientProfile(String clusterId, String organizationId, String userId, String ignoredExitNodeId) throws Exception {
|
||||
String path = "/clusters/" + clusterId + "/vpn/client-profile?organization_id=" + organizationId + "&user_id=" + userId;
|
||||
if (exitNodeId != null && !exitNodeId.trim().isEmpty()) {
|
||||
path += "&exit_node_id=" + exitNodeId.trim();
|
||||
}
|
||||
return get(path).toString();
|
||||
}
|
||||
|
||||
@@ -193,22 +109,11 @@ final class RapApiClient {
|
||||
}
|
||||
|
||||
private JSONObject get(String path) throws Exception {
|
||||
if (fabricControlManager != null) {
|
||||
return fabricControlJSON("GET", path, null);
|
||||
}
|
||||
Request request = new Request.Builder().url(baseUrl + path).get().build();
|
||||
return read(request);
|
||||
return fabricControlJSON("GET", path, null);
|
||||
}
|
||||
|
||||
private JSONObject post(String path, JSONObject body) throws Exception {
|
||||
if (fabricControlManager != null) {
|
||||
return fabricControlJSON("POST", path, body);
|
||||
}
|
||||
Request request = new Request.Builder()
|
||||
.url(baseUrl + path)
|
||||
.post(RequestBody.create(body.toString().getBytes(StandardCharsets.UTF_8), JSON))
|
||||
.build();
|
||||
return read(request);
|
||||
return fabricControlJSON("POST", path, body);
|
||||
}
|
||||
|
||||
private JSONObject fabricControlJSON(String method, String path, JSONObject body) throws Exception {
|
||||
@@ -243,7 +148,7 @@ final class RapApiClient {
|
||||
if (statusCode == 401 && bodyText.contains("auth.invalid_refresh_token")) {
|
||||
throw new IllegalStateException("Сессия устройства истекла. Введите пароль один раз.");
|
||||
}
|
||||
throw new IllegalStateException("fabric control HTTP " + statusCode + ": " + compactText(bodyText, 240));
|
||||
throw new IllegalStateException("fabric control status " + statusCode + ": " + compactText(bodyText, 240));
|
||||
}
|
||||
return bodyText.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
}
|
||||
@@ -267,42 +172,6 @@ final class RapApiClient {
|
||||
return value;
|
||||
}
|
||||
|
||||
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 JSONObject read(Request request) throws Exception {
|
||||
try (Response response = httpClient.newCall(request).execute()) {
|
||||
ResponseBody body = response.body();
|
||||
String text = body == null ? "" : body.string();
|
||||
if (!response.isSuccessful()) {
|
||||
if (response.code() == 401 && text.contains("auth.invalid_credentials")) {
|
||||
throw new IllegalStateException("Неверный логин или пароль.");
|
||||
}
|
||||
if (response.code() == 401 && text.contains("auth.invalid_refresh_token")) {
|
||||
throw new IllegalStateException("Сессия устройства истекла. Введите пароль один раз.");
|
||||
}
|
||||
throw new IllegalStateException("HTTP " + response.code() + ": " + text);
|
||||
}
|
||||
return new JSONObject(text);
|
||||
}
|
||||
}
|
||||
|
||||
private AuthContext parseAuthContext(JSONObject response) throws Exception {
|
||||
JSONObject user = response.getJSONObject("user");
|
||||
String userId = user.optString("id", "");
|
||||
|
||||
@@ -7,11 +7,14 @@ import android.content.SharedPreferences;
|
||||
import android.net.VpnService;
|
||||
import android.os.Build;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public final class RapAutostartReceiver extends BroadcastReceiver {
|
||||
private static final String PREFS = "rap-vpn";
|
||||
private static final String PREF_PROFILE_JSON = "profile_json";
|
||||
private static final String PREF_CLUSTER_ID = "cluster_id";
|
||||
private static final String PREF_VPN_CONNECTION_ID = "vpn_connection_id";
|
||||
private static final String PREF_TUNNEL_ID = "tunnel_id";
|
||||
private static final String PREF_MANUAL_STOPPED = "manual_stopped";
|
||||
|
||||
@Override
|
||||
@@ -28,13 +31,12 @@ public final class RapAutostartReceiver extends BroadcastReceiver {
|
||||
if (prefs.getBoolean(PREF_MANUAL_STOPPED, false)) {
|
||||
return;
|
||||
}
|
||||
if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(action)) {
|
||||
// After package replacement we wait for an explicit user action or runtime resume.
|
||||
return;
|
||||
}
|
||||
String profile = prefs.getString(PREF_PROFILE_JSON, "");
|
||||
String clusterId = prefs.getString(PREF_CLUSTER_ID, "");
|
||||
String vpnConnectionId = prefs.getString(PREF_VPN_CONNECTION_ID, "");
|
||||
String vpnConnectionId = prefs.getString(PREF_TUNNEL_ID, "");
|
||||
if (vpnConnectionId.isEmpty() || !profileContainsConnection(profile, vpnConnectionId)) {
|
||||
vpnConnectionId = firstConnectionId(profile);
|
||||
}
|
||||
if (profile.isEmpty() || clusterId.isEmpty() || vpnConnectionId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -44,11 +46,66 @@ public final class RapAutostartReceiver extends BroadcastReceiver {
|
||||
Intent service = new Intent(context, RapVpnService.class);
|
||||
service.putExtra("profile_json", profile);
|
||||
service.putExtra("cluster_id", clusterId);
|
||||
service.putExtra("vpn_connection_id", vpnConnectionId);
|
||||
service.putExtra("tunnel_id", vpnConnectionId);
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
context.startForegroundService(service);
|
||||
} else {
|
||||
context.startService(service);
|
||||
}
|
||||
}
|
||||
|
||||
private String firstConnectionId(String profile) {
|
||||
if (profile == null || profile.trim().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
JSONObject root = new JSONObject(profile);
|
||||
JSONObject vpnProfile = root.optJSONObject("vpn_client_profile");
|
||||
if (vpnProfile == null) {
|
||||
vpnProfile = root;
|
||||
}
|
||||
JSONArray connections = vpnProfile.optJSONArray("connections");
|
||||
if (connections == null) {
|
||||
return "";
|
||||
}
|
||||
for (int i = 0; i < connections.length(); i++) {
|
||||
JSONObject connection = connections.optJSONObject(i);
|
||||
if (connection == null) {
|
||||
continue;
|
||||
}
|
||||
String connectionId = connection.optString("id", "").trim();
|
||||
if (!connectionId.isEmpty()) {
|
||||
return connectionId;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private boolean profileContainsConnection(String profile, String connectionId) {
|
||||
if (profile == null || profile.trim().isEmpty() || connectionId == null || connectionId.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
JSONObject root = new JSONObject(profile);
|
||||
JSONObject vpnProfile = root.optJSONObject("vpn_client_profile");
|
||||
if (vpnProfile == null) {
|
||||
vpnProfile = root;
|
||||
}
|
||||
JSONArray connections = vpnProfile.optJSONArray("connections");
|
||||
if (connections == null) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < connections.length(); i++) {
|
||||
JSONObject connection = connections.optJSONObject(i);
|
||||
if (connection != null && connectionId.trim().equals(connection.optString("id", "").trim())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ public class TestVpnActivity extends Activity {
|
||||
public static final String EXTRA_PROFILE_BASE64 = "profile_base64";
|
||||
public static final String EXTRA_FABRIC_BOOTSTRAP_CONFIG = "fabric_bootstrap_config";
|
||||
public static final String EXTRA_CLUSTER_ID = "cluster_id";
|
||||
public static final String EXTRA_VPN_CONNECTION_ID = "vpn_connection_id";
|
||||
public static final String EXTRA_TUNNEL_ID = "tunnel_id";
|
||||
private static final int VPN_PREPARE_REQUEST = 77;
|
||||
|
||||
private Intent serviceIntent;
|
||||
@@ -49,7 +49,7 @@ public class TestVpnActivity extends Activity {
|
||||
intent.putExtra(RapVpnService.EXTRA_FABRIC_BOOTSTRAP_CONFIG, fabricBootstrapConfig);
|
||||
}
|
||||
intent.putExtra(RapVpnService.EXTRA_CLUSTER_ID, source.getStringExtra(EXTRA_CLUSTER_ID));
|
||||
intent.putExtra(RapVpnService.EXTRA_VPN_CONNECTION_ID, source.getStringExtra(EXTRA_VPN_CONNECTION_ID));
|
||||
intent.putExtra(RapVpnService.EXTRA_TUNNEL_ID, source.getStringExtra(EXTRA_TUNNEL_ID));
|
||||
return intent;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user