mirror of
git://f0xx.org/android_cast
synced 2026-07-29 04:18:09 +03:00
vpn service sync
This commit is contained in:
14
.cursor/rules/bottomline-reminder.mdc
Normal file
14
.cursor/rules/bottomline-reminder.mdc
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
description: Re-read bottomline_reminder.txt when owner requests implement/create/document work
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bottomline reminder (owner tasks)
|
||||||
|
|
||||||
|
When the **project owner** asks you to **implement**, **create**, or **document** something:
|
||||||
|
|
||||||
|
1. **Re-read** [bottomline_reminder.txt](bottomline_reminder.txt) at the start of that work (docs templates, alpha must-haves, UI themes/fonts, licenses, tests, buildability, task graph, submodules, atomic commits on `next`, infra safety, help_request PDFs).
|
||||||
|
2. Apply its bullets for the session unless the owner explicitly overrides.
|
||||||
|
3. Keep [third-party/](third-party/) submodules consistent (`.gitmodules`, mobile + BE licenses, Gitea mirrors, init scripts).
|
||||||
|
|
||||||
|
Do not skip this read step when switching from Q&A to implementation.
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
*.iml
|
*.iml
|
||||||
.gradle/
|
.gradle/
|
||||||
|
gradle/
|
||||||
/local.properties
|
/local.properties
|
||||||
/.idea/
|
/.idea/
|
||||||
/build/
|
/build/
|
||||||
|
|||||||
@@ -110,6 +110,12 @@ android {
|
|||||||
buildConfig true
|
buildConfig true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
resources {
|
||||||
|
excludes += '/META-INF/versions/9/OSGI-INF/MANIFEST.MF'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests {
|
unitTests {
|
||||||
includeAndroidResources = true
|
includeAndroidResources = true
|
||||||
@@ -133,7 +139,5 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'com.github.mwiede:jsch:0.2.21'
|
implementation 'com.github.mwiede:jsch:0.2.21'
|
||||||
|
|
||||||
if (project.findProject(':tunnel') != null) {
|
implementation project(':tunnel')
|
||||||
implementation project(':tunnel')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
@@ -133,9 +134,16 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="connectedDevice" />
|
android:foregroundServiceType="connectedDevice" />
|
||||||
|
|
||||||
|
<!-- VPN stack: isolated process com.foxx.androidcast:vpn (AIDL via AndroidCastVpnService). -->
|
||||||
<service
|
<service
|
||||||
android:name=".remoteaccess.RemoteAccessVpnService"
|
android:name=".vpn.AndroidCastVpnService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
android:process=":vpn" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".vpn.RemoteAccessVpnService"
|
||||||
|
android:exported="false"
|
||||||
|
android:process=":vpn"
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
android:foregroundServiceType="connectedDevice">
|
android:foregroundServiceType="connectedDevice">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -143,6 +151,17 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="com.wireguard.android.backend.GoBackend$VpnService"
|
||||||
|
android:exported="false"
|
||||||
|
android:process=":vpn"
|
||||||
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
|
tools:node="merge">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.net.VpnService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".TrayNotificationReceiver"
|
android:name=".TrayNotificationReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.foxx.androidcast;
|
||||||
|
|
||||||
|
/** One-shot result for apply / tearDown. */
|
||||||
|
interface IVpnOperationCallback {
|
||||||
|
oneway void onSuccess();
|
||||||
|
oneway void onFailure(String reason);
|
||||||
|
}
|
||||||
19
app/src/main/aidl/com/foxx/androidcast/IVpnService.aidl
Normal file
19
app/src/main/aidl/com/foxx/androidcast/IVpnService.aidl
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package com.foxx.androidcast;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.IVpnOperationCallback;
|
||||||
|
import com.foxx.androidcast.IVpnStateCallback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone VPN control plane (process com.foxx.androidcast:vpn).
|
||||||
|
* WireGuard userspace + TUN fallback run only in the VPN process.
|
||||||
|
*/
|
||||||
|
interface IVpnService {
|
||||||
|
void registerStateCallback(IVpnStateCallback callback);
|
||||||
|
void unregisterStateCallback(IVpnStateCallback callback);
|
||||||
|
int getState();
|
||||||
|
boolean isVpnPermissionGranted();
|
||||||
|
void applyWireGuardConfig(String wgQuickConfig, IVpnOperationCallback callback);
|
||||||
|
void tearDown(IVpnOperationCallback callback);
|
||||||
|
/** Debug/lab only — records a synthetic crash from the VPN process (no process kill). */
|
||||||
|
void simulateCrashForTest(int sequence, IVpnOperationCallback callback);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.foxx.androidcast;
|
||||||
|
|
||||||
|
/** Async VPN state updates from the :vpn process. */
|
||||||
|
interface IVpnStateCallback {
|
||||||
|
oneway void onStateChanged(int state, String detail);
|
||||||
|
}
|
||||||
@@ -32,5 +32,5 @@
|
|||||||
<li><b>Speex</b> — Revised BSD license (Xiph.org Foundation). See <code>third-party/speex/COPYING</code>.</li>
|
<li><b>Speex</b> — Revised BSD license (Xiph.org Foundation). See <code>third-party/speex/COPYING</code>.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>WireGuard tunnel module (optional Gradle module)</h2>
|
<h2>WireGuard tunnel module (<code>third-party/wireguard-android</code>)</h2>
|
||||||
<p>When <code>third-party/wireguard-android</code> is initialized and the <code>:tunnel</code> module is included, the app may link the WireGuard Android tunnel library (Apache License 2.0; see upstream <code>wireguard-android</code>). Remote access still uses Android <code>VpnService</code> (system VPN permission).</p>
|
<p>The app links the WireGuard Android tunnel library from git submodule <code>third-party/wireguard-android</code> (Apache License 2.0; see upstream <code>COPYING</code> and <code>https://git.zx2c4.com/wireguard-android</code>). WireGuard lab mode uses Android <code>VpnService</code> (system VPN permission) via <code>GoBackend</code>.</p>
|
||||||
|
|||||||
@@ -32,10 +32,13 @@ public class AndroidCastApplication extends Application {
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
CrashReporter.install(this);
|
||||||
|
if (!CrashReporter.isMainProcess(this, CrashReporter.currentProcessName())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
CodecPriorityCatalog.init(this);
|
CodecPriorityCatalog.init(this);
|
||||||
CastLocaleHelper.applyStoredLocale(this);
|
CastLocaleHelper.applyStoredLocale(this);
|
||||||
CastThemeHelper.applyNightMode(this);
|
CastThemeHelper.applyNightMode(this);
|
||||||
CrashReporter.install(this);
|
|
||||||
ReceiverAvRuntime.reload(this);
|
ReceiverAvRuntime.reload(this);
|
||||||
CastTransportFactory.init(this);
|
CastTransportFactory.init(this);
|
||||||
WiredDisplayMonitor.getInstance(this).ensureRegistered();
|
WiredDisplayMonitor.getInstance(this).ensureRegistered();
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
package com.foxx.androidcast.crash;
|
package com.foxx.androidcast.crash;
|
||||||
|
|
||||||
/*********************************************************************
|
|
||||||
* CrashReporter.java
|
|
||||||
* Created at: Wed 20 May 2026 14:31:55 +0200
|
|
||||||
* Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
|
|
||||||
* Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
|
|
||||||
* class CrashReporter
|
|
||||||
* Contributors:
|
|
||||||
* - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 89 lines)
|
|
||||||
* - Cursor Agent (project assistant)
|
|
||||||
* Digest: SHA256 5254fee83476d4a80db718169cbc13b61b18697148a8625baab8df0833b1d78f
|
|
||||||
**********************************************************************/
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -21,19 +10,29 @@ import org.json.JSONObject;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
/** Main-process entry: write pending crash JSON and wake the watcher process. */
|
/**
|
||||||
|
* Crash capture for main and monitored isolated processes ({@code :vpn}, {@code :netselftest}).
|
||||||
|
* Persists JSON to shared app storage and wakes {@link CrashWatcherService}.
|
||||||
|
*/
|
||||||
public final class CrashReporter {
|
public final class CrashReporter {
|
||||||
private static final String TAG = "CrashReporter";
|
private static final String TAG = "CrashReporter";
|
||||||
|
|
||||||
private CrashReporter() {}
|
private CrashReporter() {}
|
||||||
|
|
||||||
|
/** Install handlers appropriate for the current process. */
|
||||||
public static void install(Context context) {
|
public static void install(Context context) {
|
||||||
Context app = context.getApplicationContext();
|
Context app = context.getApplicationContext();
|
||||||
if (!isMainProcess(app)) {
|
String proc = currentProcessName();
|
||||||
return;
|
if (isMainProcess(app, proc)) {
|
||||||
|
installMainProcess(app);
|
||||||
|
} else if (isMonitoredIsolatedProcess(proc)) {
|
||||||
|
installIsolatedProcess(app);
|
||||||
}
|
}
|
||||||
if (app instanceof android.app.Application) {
|
}
|
||||||
CrashRuntimeContext.init((android.app.Application) app);
|
|
||||||
|
private static void installMainProcess(Context app) {
|
||||||
|
if (app instanceof Application) {
|
||||||
|
CrashRuntimeContext.init((Application) app);
|
||||||
}
|
}
|
||||||
CrashNativeBridge.init(app);
|
CrashNativeBridge.init(app);
|
||||||
final Thread.UncaughtExceptionHandler previous =
|
final Thread.UncaughtExceptionHandler previous =
|
||||||
@@ -55,6 +54,25 @@ public final class CrashReporter {
|
|||||||
CrashWatcherService.scheduleUploadSweep(app);
|
CrashWatcherService.scheduleUploadSweep(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void installIsolatedProcess(Context app) {
|
||||||
|
CrashNativeBridge.init(app);
|
||||||
|
final Thread.UncaughtExceptionHandler previous =
|
||||||
|
Thread.getDefaultUncaughtExceptionHandler();
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
|
||||||
|
try {
|
||||||
|
recordJavaCrash(app, thread, throwable);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
Log.e(TAG, "isolated recordJavaCrash failed", t);
|
||||||
|
}
|
||||||
|
if (previous != null) {
|
||||||
|
previous.uncaughtException(thread, throwable);
|
||||||
|
} else {
|
||||||
|
System.exit(10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Log.i(TAG, "crash capture enabled in " + currentProcessName());
|
||||||
|
}
|
||||||
|
|
||||||
public static void recordJavaCrash(Context context, Thread thread, Throwable throwable) {
|
public static void recordJavaCrash(Context context, Thread thread, Throwable throwable) {
|
||||||
if (!CrashSettingsStore.load(context).enabled) {
|
if (!CrashSettingsStore.load(context).enabled) {
|
||||||
return;
|
return;
|
||||||
@@ -95,7 +113,22 @@ public final class CrashReporter {
|
|||||||
CrashWatcherService.requestImmediateSweep(context, true);
|
CrashWatcherService.requestImmediateSweep(context, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isMainProcess(Context context) {
|
public static boolean isMainProcess(Context context, String procName) {
|
||||||
return context.getPackageName().equals(Application.getProcessName());
|
return context.getPackageName().equals(procName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isMonitoredIsolatedProcess(String procName) {
|
||||||
|
if (procName == null || procName.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return procName.endsWith(":vpn") || procName.endsWith(":netselftest");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String currentProcessName() {
|
||||||
|
try {
|
||||||
|
return Application.getProcessName();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,13 @@ public final class CrashRuntimeContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String scenarioLabel() {
|
public static String scenarioLabel() {
|
||||||
|
String proc = CrashReporter.currentProcessName();
|
||||||
|
if (proc != null && proc.endsWith(":vpn")) {
|
||||||
|
return "vpn_service";
|
||||||
|
}
|
||||||
|
if (proc != null && proc.endsWith(":netselftest")) {
|
||||||
|
return "net_selftest";
|
||||||
|
}
|
||||||
if (SessionStatsContext.get() != null || isCastSessionActive()) {
|
if (SessionStatsContext.get() != null || isCastSessionActive()) {
|
||||||
return "cast_session";
|
return "cast_session";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.foxx.androidcast.AppPreferences;
|
import com.foxx.androidcast.AppPreferences;
|
||||||
|
import com.foxx.androidcast.vpn.VpnServiceClient;
|
||||||
|
|
||||||
/** Start/stop remote access foreground polling + VPN based on developer mode. */
|
/** Start/stop remote access foreground polling + VPN based on developer mode. */
|
||||||
public final class RemoteAccessCoordinator {
|
public final class RemoteAccessCoordinator {
|
||||||
@@ -17,7 +18,7 @@ public final class RemoteAccessCoordinator {
|
|||||||
Log.i(TAG, "disabling remote access");
|
Log.i(TAG, "disabling remote access");
|
||||||
RemoteAccessService.sendDisableOnce(app);
|
RemoteAccessService.sendDisableOnce(app);
|
||||||
RemoteAccessService.stop(app);
|
RemoteAccessService.stop(app);
|
||||||
RemoteAccessVpnService.stop(app);
|
VpnServiceClient.get(app).tearDown();
|
||||||
AppPreferences.clearDevRemoteAccessSession(app);
|
AppPreferences.clearDevRemoteAccessSession(app);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import com.foxx.androidcast.AppPreferences;
|
|||||||
import com.foxx.androidcast.DeveloperSettingsActivity;
|
import com.foxx.androidcast.DeveloperSettingsActivity;
|
||||||
import com.foxx.androidcast.DeviceInfo;
|
import com.foxx.androidcast.DeviceInfo;
|
||||||
import com.foxx.androidcast.R;
|
import com.foxx.androidcast.R;
|
||||||
|
import com.foxx.androidcast.vpn.VpnServiceClient;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
@@ -185,20 +186,29 @@ public final class RemoteAccessService extends Service {
|
|||||||
if (creds == null) {
|
if (creds == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boolean ok = false;
|
|
||||||
if ("wireguard".equals(tunnel)) {
|
if ("wireguard".equals(tunnel)) {
|
||||||
String wgConfig = WireGuardConfigBuilder.fromConnectCredentials(creds);
|
String wgConfig = WireGuardConfigBuilder.fromConnectCredentials(creds);
|
||||||
if (!wgConfig.isEmpty()) {
|
if (wgConfig.isEmpty()) {
|
||||||
ok = WireGuardTunnelBridge.applyConfig(this, wgConfig);
|
return;
|
||||||
}
|
}
|
||||||
|
final String sid = sessionId;
|
||||||
|
VpnServiceClient.get(this).applyWireGuardConfig(wgConfig, new VpnServiceClient.OperationListener() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess() {
|
||||||
|
updateNotificationConnected(sid, tunnel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(String reason) {
|
||||||
|
Log.w(TAG, "wireguard connect failed: " + reason);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else if ("ssh_reverse".equals(tunnel)) {
|
} else if ("ssh_reverse".equals(tunnel)) {
|
||||||
ok = ReverseSshTunnelBridge.applyConfig(this, creds);
|
if (ReverseSshTunnelBridge.applyConfig(this, creds)) {
|
||||||
|
updateNotificationConnected(sessionId, tunnel);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "unsupported tunnel in connect: " + tunnel);
|
Log.w(TAG, "unsupported tunnel in connect: " + tunnel);
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ok) {
|
|
||||||
updateNotificationConnected(sessionId, tunnel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package com.foxx.androidcast.remoteaccess;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.vpn.VpnServiceClient;
|
||||||
|
|
||||||
/** Tear down all remote-access transports (WireGuard + RSSH). */
|
/** Tear down all remote-access transports (WireGuard + RSSH). */
|
||||||
public final class RemoteAccessTunnelHelper {
|
public final class RemoteAccessTunnelHelper {
|
||||||
private RemoteAccessTunnelHelper() {}
|
private RemoteAccessTunnelHelper() {}
|
||||||
|
|
||||||
public static void tearDownAll(Context context) {
|
public static void tearDownAll(Context context) {
|
||||||
WireGuardTunnelBridge.tearDown(context);
|
VpnServiceClient.get(context).tearDown();
|
||||||
ReverseSshTunnelBridge.tearDown(context);
|
ReverseSshTunnelBridge.tearDown(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
package com.foxx.androidcast.remoteaccess;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional integration with wireguard-android tunnel module (third-party/wireguard-android).
|
|
||||||
* Falls back to {@link RemoteAccessVpnService} TUN setup when module is absent.
|
|
||||||
*/
|
|
||||||
public final class WireGuardTunnelBridge {
|
|
||||||
private static final String TAG = "WGTunnelBridge";
|
|
||||||
|
|
||||||
private WireGuardTunnelBridge() {}
|
|
||||||
|
|
||||||
public static boolean applyConfig(Context context, String wgQuickConfig) {
|
|
||||||
if (wgQuickConfig == null || wgQuickConfig.trim().isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Class<?> configClass = Class.forName("com.wireguard.config.Config");
|
|
||||||
Object config = configClass.getMethod("parse", String.class).invoke(null, wgQuickConfig);
|
|
||||||
Class<?> goBackendClass = Class.forName("com.wireguard.android.backend.GoBackend");
|
|
||||||
Object backend = goBackendClass.getConstructor(Context.class).newInstance(context);
|
|
||||||
Class<?> tunnelClass = Class.forName("com.wireguard.android.backend.Tunnel");
|
|
||||||
Object tunnel = proxyTunnel(tunnelClass);
|
|
||||||
Class<?> stateClass = Class.forName("com.wireguard.android.backend.Tunnel$State");
|
|
||||||
Object up = enumConstant(stateClass, "UP");
|
|
||||||
goBackendClass.getMethod("setState", tunnelClass, stateClass, configClass)
|
|
||||||
.invoke(backend, tunnel, up, config);
|
|
||||||
Log.i(TAG, "wireguard tunnel UP via GoBackend");
|
|
||||||
return true;
|
|
||||||
} catch (Throwable t) {
|
|
||||||
Log.i(TAG, "GoBackend unavailable, using VpnService fallback: " + t.getClass().getSimpleName());
|
|
||||||
return RemoteAccessVpnService.startWithConfig(context, wgQuickConfig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void tearDown(Context context) {
|
|
||||||
RemoteAccessVpnService.stop(context);
|
|
||||||
try {
|
|
||||||
Class<?> goBackendClass = Class.forName("com.wireguard.android.backend.GoBackend");
|
|
||||||
Object backend = goBackendClass.getConstructor(Context.class).newInstance(context);
|
|
||||||
Class<?> tunnelClass = Class.forName("com.wireguard.android.backend.Tunnel");
|
|
||||||
Object tunnel = proxyTunnel(tunnelClass);
|
|
||||||
Class<?> stateClass = Class.forName("com.wireguard.android.backend.Tunnel$State");
|
|
||||||
Object down = enumConstant(stateClass, "DOWN");
|
|
||||||
goBackendClass.getMethod("setState", tunnelClass, stateClass, Class.forName("com.wireguard.config.Config"))
|
|
||||||
.invoke(backend, tunnel, down, null);
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object proxyTunnel(Class<?> tunnelClass) {
|
|
||||||
return java.lang.reflect.Proxy.newProxyInstance(
|
|
||||||
tunnelClass.getClassLoader(),
|
|
||||||
new Class<?>[]{tunnelClass},
|
|
||||||
(proxy, method, args) -> "getName".equals(method.getName()) ? "androidcast-ra" : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
|
||||||
private static Object enumConstant(Class<?> stateClass, String name) {
|
|
||||||
return Enum.valueOf((Class<? extends Enum>) stateClass, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package com.foxx.androidcast.vpn;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.VpnService;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.RemoteCallbackList;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.IVpnOperationCallback;
|
||||||
|
import com.foxx.androidcast.IVpnService;
|
||||||
|
import com.foxx.androidcast.IVpnStateCallback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone VPN process ({@code com.foxx.androidcast:vpn}) — AIDL control plane for WireGuard.
|
||||||
|
*/
|
||||||
|
public final class AndroidCastVpnService extends Service {
|
||||||
|
private static final String TAG = "AndroidCastVpn";
|
||||||
|
/** Debug/lab: record synthetic VPN-process crash (see scripts/simulate-vpn-crashes.sh). */
|
||||||
|
public static final String ACTION_SIMULATE_CRASH = "com.foxx.androidcast.vpn.SIMULATE_CRASH";
|
||||||
|
public static final String EXTRA_SIM_SEQ = "sim_seq";
|
||||||
|
|
||||||
|
private final RemoteCallbackList<IVpnStateCallback> stateCallbacks = new RemoteCallbackList<>();
|
||||||
|
private final Object stateLock = new Object();
|
||||||
|
private volatile int state = VpnState.DOWN;
|
||||||
|
private volatile String stateDetail = "";
|
||||||
|
|
||||||
|
private HandlerThread workerThread;
|
||||||
|
private Handler worker;
|
||||||
|
|
||||||
|
private final IVpnService.Stub binder = new IVpnService.Stub() {
|
||||||
|
@Override
|
||||||
|
public void registerStateCallback(IVpnStateCallback callback) {
|
||||||
|
if (callback != null) {
|
||||||
|
stateCallbacks.register(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unregisterStateCallback(IVpnStateCallback callback) {
|
||||||
|
if (callback != null) {
|
||||||
|
stateCallbacks.unregister(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isVpnPermissionGranted() {
|
||||||
|
return VpnService.prepare(AndroidCastVpnService.this) == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyWireGuardConfig(String wgQuickConfig, IVpnOperationCallback callback) {
|
||||||
|
ensureWorker();
|
||||||
|
setState(VpnState.CONNECTING, "applying wireguard config");
|
||||||
|
worker.post(() -> {
|
||||||
|
boolean ok = WireGuardVpnEngine.apply(getApplicationContext(), wgQuickConfig);
|
||||||
|
if (ok) {
|
||||||
|
setState(VpnState.UP, "wireguard tunnel up");
|
||||||
|
notifyOperation(callback, true, null);
|
||||||
|
} else {
|
||||||
|
setState(VpnState.ERROR, "wireguard apply failed");
|
||||||
|
notifyOperation(callback, false, "apply failed or VPN permission missing");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tearDown(IVpnOperationCallback callback) {
|
||||||
|
ensureWorker();
|
||||||
|
worker.post(() -> {
|
||||||
|
WireGuardVpnEngine.tearDown(getApplicationContext());
|
||||||
|
setState(VpnState.DOWN, "tunnel down");
|
||||||
|
notifyOperation(callback, true, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void simulateCrashForTest(int sequence, IVpnOperationCallback callback) {
|
||||||
|
ensureWorker();
|
||||||
|
worker.post(() -> {
|
||||||
|
boolean ok = VpnCrashSimulator.recordSimulatedCrash(getApplicationContext(), sequence);
|
||||||
|
if (ok) {
|
||||||
|
notifyOperation(callback, true, null);
|
||||||
|
} else {
|
||||||
|
notifyOperation(callback, false, "simulate only in debug builds");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
Log.i(TAG, "VPN process service created");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return binder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
if (intent != null && ACTION_SIMULATE_CRASH.equals(intent.getAction())) {
|
||||||
|
int seq = intent.getIntExtra(EXTRA_SIM_SEQ, 0);
|
||||||
|
ensureWorker();
|
||||||
|
worker.post(() -> {
|
||||||
|
VpnCrashSimulator.recordSimulatedCrash(getApplicationContext(), seq);
|
||||||
|
stopSelf(startId);
|
||||||
|
});
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
return START_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
if (workerThread != null) {
|
||||||
|
workerThread.quitSafely();
|
||||||
|
workerThread = null;
|
||||||
|
worker = null;
|
||||||
|
}
|
||||||
|
WireGuardVpnEngine.tearDown(getApplicationContext());
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureWorker() {
|
||||||
|
if (worker != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
workerThread = new HandlerThread("androidcast-vpn-worker");
|
||||||
|
workerThread.start();
|
||||||
|
worker = new Handler(workerThread.getLooper());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setState(int newState, String detail) {
|
||||||
|
synchronized (stateLock) {
|
||||||
|
state = newState;
|
||||||
|
stateDetail = detail != null ? detail : "";
|
||||||
|
}
|
||||||
|
broadcastState(newState, stateDetail);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void broadcastState(int newState, String detail) {
|
||||||
|
int n = stateCallbacks.beginBroadcast();
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
try {
|
||||||
|
stateCallbacks.getBroadcastItem(i).onStateChanged(newState, detail);
|
||||||
|
} catch (RemoteException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
stateCallbacks.finishBroadcast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void notifyOperation(IVpnOperationCallback callback, boolean ok, String reason) {
|
||||||
|
if (callback == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (ok) {
|
||||||
|
callback.onSuccess();
|
||||||
|
} else {
|
||||||
|
callback.onFailure(reason != null ? reason : "error");
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.w(TAG, "operation callback died: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.foxx.androidcast.remoteaccess;
|
package com.foxx.androidcast.vpn;
|
||||||
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
@@ -18,14 +18,15 @@ import com.foxx.androidcast.DeveloperSettingsActivity;
|
|||||||
import com.foxx.androidcast.R;
|
import com.foxx.androidcast.R;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/** Holds WireGuard TUN when tunnel module is not linked; also prepares interface for userspace WG. */
|
/** TUN fallback when GoBackend cannot start — runs in :vpn process only. */
|
||||||
public final class RemoteAccessVpnService extends VpnService {
|
public final class RemoteAccessVpnService extends VpnService {
|
||||||
private static final String TAG = "RemoteAccessVPN";
|
private static final String TAG = "RemoteAccessVPN";
|
||||||
public static final String ACTION_UP = "com.foxx.androidcast.remoteaccess.VPN_UP";
|
public static final String ACTION_UP = "com.foxx.androidcast.vpn.VPN_UP";
|
||||||
public static final String ACTION_DOWN = "com.foxx.androidcast.remoteaccess.VPN_DOWN";
|
public static final String ACTION_DOWN = "com.foxx.androidcast.vpn.VPN_DOWN";
|
||||||
public static final String EXTRA_WG_CONFIG = "wg_config";
|
public static final String EXTRA_WG_CONFIG = "wg_config";
|
||||||
|
|
||||||
private static final int NOTIF_ID = 0x7a01;
|
private static final int NOTIF_ID = 0x7a01;
|
||||||
@@ -108,7 +109,6 @@ public final class RemoteAccessVpnService extends VpnService {
|
|||||||
return m.find() ? m.group(1).trim() : null;
|
return m.find() ? m.group(1).trim() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Routes from AllowedIPs= lines (split tunnel — typically server wg /32). */
|
|
||||||
private static String[] parseAllowedRoutes(String wgConfig) {
|
private static String[] parseAllowedRoutes(String wgConfig) {
|
||||||
if (wgConfig == null) {
|
if (wgConfig == null) {
|
||||||
return new String[0];
|
return new String[0];
|
||||||
@@ -118,7 +118,7 @@ public final class RemoteAccessVpnService extends VpnService {
|
|||||||
return new String[0];
|
return new String[0];
|
||||||
}
|
}
|
||||||
String[] parts = m.group(1).trim().split("\\s*,\\s*");
|
String[] parts = m.group(1).trim().split("\\s*,\\s*");
|
||||||
java.util.ArrayList<String> out = new java.util.ArrayList<>();
|
ArrayList<String> out = new ArrayList<>();
|
||||||
for (String p : parts) {
|
for (String p : parts) {
|
||||||
if (!p.isEmpty()) {
|
if (!p.isEmpty()) {
|
||||||
out.add(p.trim());
|
out.add(p.trim());
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.foxx.androidcast.vpn;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.BuildConfig;
|
||||||
|
import com.foxx.androidcast.crash.CrashReporter;
|
||||||
|
import com.foxx.androidcast.crash.CrashWatcherService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records synthetic Java crashes from the VPN process for lab validation (debug builds only).
|
||||||
|
* Does not kill the process — exercises persist + watcher upload path.
|
||||||
|
*/
|
||||||
|
public final class VpnCrashSimulator {
|
||||||
|
private VpnCrashSimulator() {}
|
||||||
|
|
||||||
|
public static boolean recordSimulatedCrash(Context context, int sequence) {
|
||||||
|
if (!BuildConfig.DEBUG) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RuntimeException ex = new RuntimeException(
|
||||||
|
"VpnCrashSimulation seq=" + sequence + " proc=" + CrashReporter.currentProcessName());
|
||||||
|
ex.setStackTrace(buildStack(sequence));
|
||||||
|
CrashReporter.recordJavaCrash(context, Thread.currentThread(), ex);
|
||||||
|
CrashWatcherService.requestImmediateSweep(context.getApplicationContext(), true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StackTraceElement[] buildStack(int sequence) {
|
||||||
|
return new StackTraceElement[]{
|
||||||
|
new StackTraceElement("com.foxx.androidcast.vpn.WireGuardVpnEngine", "apply",
|
||||||
|
"WireGuardVpnEngine.java", 40 + (sequence % 5)),
|
||||||
|
new StackTraceElement("com.foxx.androidcast.vpn.AndroidCastVpnService$1", "applyWireGuardConfig",
|
||||||
|
"AndroidCastVpnService.java", 60),
|
||||||
|
new StackTraceElement("com.foxx.androidcast.vpn.AndroidCastVpnService", "onStartCommand",
|
||||||
|
"AndroidCastVpnService.java", 95 + (sequence % 3)),
|
||||||
|
new StackTraceElement("android.app.ActivityThread", "handleServiceArgs",
|
||||||
|
"ActivityThread.java", 5001),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
241
app/src/main/java/com/foxx/androidcast/vpn/VpnServiceClient.java
Normal file
241
app/src/main/java/com/foxx/androidcast/vpn/VpnServiceClient.java
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
package com.foxx.androidcast.vpn;
|
||||||
|
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.IVpnOperationCallback;
|
||||||
|
import com.foxx.androidcast.IVpnService;
|
||||||
|
import com.foxx.androidcast.IVpnStateCallback;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main-process client for {@link AndroidCastVpnService} ({@code com.foxx.androidcast:vpn}).
|
||||||
|
*/
|
||||||
|
public final class VpnServiceClient {
|
||||||
|
private static final String TAG = "VpnServiceClient";
|
||||||
|
private static final long BIND_TIMEOUT_MS = 8_000L;
|
||||||
|
|
||||||
|
private static volatile VpnServiceClient instance;
|
||||||
|
|
||||||
|
private final Context appContext;
|
||||||
|
private final Handler main = new Handler(Looper.getMainLooper());
|
||||||
|
private final Object bindLock = new Object();
|
||||||
|
private final List<Runnable> pending = new ArrayList<>();
|
||||||
|
|
||||||
|
private IVpnService service;
|
||||||
|
private boolean binding;
|
||||||
|
private CountDownLatch bindLatch;
|
||||||
|
|
||||||
|
public interface StateListener {
|
||||||
|
void onStateChanged(int state, String detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OperationListener {
|
||||||
|
void onSuccess();
|
||||||
|
|
||||||
|
void onFailure(String reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private VpnServiceClient(Context context) {
|
||||||
|
appContext = context.getApplicationContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VpnServiceClient get(Context context) {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (VpnServiceClient.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new VpnServiceClient(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ServiceConnection connection = new ServiceConnection() {
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder binder) {
|
||||||
|
synchronized (bindLock) {
|
||||||
|
service = IVpnService.Stub.asInterface(binder);
|
||||||
|
binding = false;
|
||||||
|
if (bindLatch != null) {
|
||||||
|
bindLatch.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushPending();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
synchronized (bindLock) {
|
||||||
|
service = null;
|
||||||
|
binding = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public boolean isVpnPermissionGranted() {
|
||||||
|
if (!ensureBoundSync()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return service.isVpnPermissionGranted();
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.w(TAG, "isVpnPermissionGranted: " + e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getState() {
|
||||||
|
if (!ensureBoundSync()) {
|
||||||
|
return VpnState.DOWN;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return service.getState();
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
return VpnState.DOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerStateListener(StateListener listener) {
|
||||||
|
runWithService(s -> {
|
||||||
|
IVpnStateCallback cb = new IVpnStateCallback.Stub() {
|
||||||
|
@Override
|
||||||
|
public void onStateChanged(int state, String detail) {
|
||||||
|
main.post(() -> listener.onStateChanged(state, detail));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
s.registerStateCallback(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyWireGuardConfig(String wgQuickConfig, OperationListener listener) {
|
||||||
|
runWithService(s -> {
|
||||||
|
try {
|
||||||
|
s.applyWireGuardConfig(wgQuickConfig, wrapOperation(listener));
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
deliverOperation(listener, false, e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tearDown(OperationListener listener) {
|
||||||
|
runWithService(s -> {
|
||||||
|
try {
|
||||||
|
s.tearDown(wrapOperation(listener));
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
deliverOperation(listener, false, e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tearDown() {
|
||||||
|
tearDown(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IVpnOperationCallback wrapOperation(OperationListener listener) {
|
||||||
|
return new IVpnOperationCallback.Stub() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess() {
|
||||||
|
deliverOperation(listener, true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(String reason) {
|
||||||
|
deliverOperation(listener, false, reason);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deliverOperation(OperationListener listener, boolean ok, String reason) {
|
||||||
|
if (listener == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
main.post(() -> {
|
||||||
|
if (ok) {
|
||||||
|
listener.onSuccess();
|
||||||
|
} else {
|
||||||
|
listener.onFailure(reason != null ? reason : "error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ServiceTask {
|
||||||
|
void run(IVpnService s) throws RemoteException;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runWithService(ServiceTask task) {
|
||||||
|
IVpnService s = service;
|
||||||
|
if (s != null) {
|
||||||
|
try {
|
||||||
|
task.run(s);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.w(TAG, "remote call failed: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (bindLock) {
|
||||||
|
pending.add(() -> runWithService(task));
|
||||||
|
}
|
||||||
|
ensureBoundAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushPending() {
|
||||||
|
List<Runnable> copy;
|
||||||
|
synchronized (bindLock) {
|
||||||
|
copy = new ArrayList<>(pending);
|
||||||
|
pending.clear();
|
||||||
|
}
|
||||||
|
for (Runnable r : copy) {
|
||||||
|
r.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureBoundAsync() {
|
||||||
|
synchronized (bindLock) {
|
||||||
|
if (service != null || binding) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
binding = true;
|
||||||
|
Intent i = new Intent(appContext, AndroidCastVpnService.class);
|
||||||
|
appContext.startService(i);
|
||||||
|
appContext.bindService(i, connection, Context.BIND_AUTO_CREATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean ensureBoundSync() {
|
||||||
|
if (service != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
synchronized (bindLock) {
|
||||||
|
if (service != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bindLatch = new CountDownLatch(1);
|
||||||
|
ensureBoundAsync();
|
||||||
|
try {
|
||||||
|
if (!bindLatch.await(BIND_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
|
||||||
|
Log.w(TAG, "VPN service bind timeout");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
bindLatch = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return service != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/src/main/java/com/foxx/androidcast/vpn/VpnState.java
Normal file
11
app/src/main/java/com/foxx/androidcast/vpn/VpnState.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package com.foxx.androidcast.vpn;
|
||||||
|
|
||||||
|
/** VPN state codes shared across AIDL ({@link com.foxx.androidcast.IVpnService}). */
|
||||||
|
public final class VpnState {
|
||||||
|
public static final int DOWN = 0;
|
||||||
|
public static final int CONNECTING = 1;
|
||||||
|
public static final int UP = 2;
|
||||||
|
public static final int ERROR = 3;
|
||||||
|
|
||||||
|
private VpnState() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.foxx.androidcast.vpn;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.wireguard.android.backend.GoBackend;
|
||||||
|
import com.wireguard.android.backend.Tunnel;
|
||||||
|
import com.wireguard.config.Config;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/** WireGuard tunnel control inside the :vpn process only. */
|
||||||
|
final class WireGuardVpnEngine {
|
||||||
|
private static final String TAG = "WireGuardVpnEngine";
|
||||||
|
private static final Tunnel ANDROID_CAST_TUNNEL = new Tunnel() {
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "androidcast-ra";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStateChange(State newState) {
|
||||||
|
Log.d(TAG, "tunnel state " + newState);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private WireGuardVpnEngine() {}
|
||||||
|
|
||||||
|
static boolean apply(Context context, String wgQuickConfig) {
|
||||||
|
if (wgQuickConfig == null || wgQuickConfig.trim().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Config config;
|
||||||
|
try (ByteArrayInputStream in = new ByteArrayInputStream(
|
||||||
|
wgQuickConfig.getBytes(StandardCharsets.UTF_8))) {
|
||||||
|
config = Config.parse(in);
|
||||||
|
}
|
||||||
|
GoBackend backend = new GoBackend(context);
|
||||||
|
backend.setState(ANDROID_CAST_TUNNEL, Tunnel.State.UP, config);
|
||||||
|
Log.i(TAG, "wireguard UP via GoBackend");
|
||||||
|
return true;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
Log.w(TAG, "GoBackend failed, TUN fallback: " + t.getMessage());
|
||||||
|
return RemoteAccessVpnService.startWithConfig(context, wgQuickConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tearDown(Context context) {
|
||||||
|
RemoteAccessVpnService.stop(context);
|
||||||
|
try {
|
||||||
|
GoBackend backend = new GoBackend(context);
|
||||||
|
backend.setState(ANDROID_CAST_TUNNEL, Tunnel.State.DOWN, null);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.foxx.androidcast.crash;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class CrashReporterProcessTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void isMonitoredIsolatedProcess_vpnAndNetselftest() {
|
||||||
|
assertTrue(CrashReporter.isMonitoredIsolatedProcess("com.foxx.androidcast:vpn"));
|
||||||
|
assertTrue(CrashReporter.isMonitoredIsolatedProcess("com.foxx.androidcast:netselftest"));
|
||||||
|
assertFalse(CrashReporter.isMonitoredIsolatedProcess("com.foxx.androidcast"));
|
||||||
|
assertFalse(CrashReporter.isMonitoredIsolatedProcess("com.foxx.androidcast:crashwatcher"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.foxx.androidcast.remoteaccess;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
|
||||||
|
import com.wireguard.config.Config;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/** Ensures BE credential → wg-quick text parses with wireguard-android Config. */
|
||||||
|
public class WireGuardConfigParseTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void builderOutput_parsesWithWireGuardConfig() throws Exception {
|
||||||
|
JSONObject creds = new JSONObject();
|
||||||
|
creds.put("interface_private_key", "YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
|
||||||
|
creds.put("interface_address", "10.66.66.2/32");
|
||||||
|
creds.put("peer_public_key", "YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
|
||||||
|
creds.put("peer_endpoint", "ra.apps.f0xx.org:51820");
|
||||||
|
creds.put("peer_allowed_ips", "10.66.66.1/32");
|
||||||
|
|
||||||
|
String quick = WireGuardConfigBuilder.fromConnectCredentials(creds);
|
||||||
|
assertFalse(quick.isEmpty());
|
||||||
|
|
||||||
|
try (ByteArrayInputStream in = new ByteArrayInputStream(quick.getBytes(StandardCharsets.UTF_8))) {
|
||||||
|
Config config = Config.parse(in);
|
||||||
|
assertEquals("10.66.66.2/32", config.getInterface().getAddresses().iterator().next().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/src/test/java/com/foxx/androidcast/vpn/VpnStateTest.java
Normal file
15
app/src/test/java/com/foxx/androidcast/vpn/VpnStateTest.java
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package com.foxx.androidcast.vpn;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class VpnStateTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void stateCodes_areDistinct() {
|
||||||
|
assertTrue(VpnState.DOWN < VpnState.CONNECTING);
|
||||||
|
assertTrue(VpnState.CONNECTING < VpnState.UP);
|
||||||
|
assertTrue(VpnState.UP < VpnState.ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,17 @@ cd orchestration
|
|||||||
|
|
||||||
Direct debug ports: crashes `:8082`, builder `:8083`.
|
Direct debug ports: crashes `:8082`, builder `:8083`.
|
||||||
|
|
||||||
|
## Local developer rebuild
|
||||||
|
|
||||||
|
`./rebuild.sh` (repo root):
|
||||||
|
|
||||||
|
1. `scripts/init-third-party-submodules.sh` — libvpx, opus, speex, **wireguard-android** (+ nested wireguard-tools / elf-cleaner)
|
||||||
|
2. `scripts/build-native-codecs.sh` per ABI
|
||||||
|
3. `./gradlew :tunnel:assembleDebug :app:assembleDebug` — WireGuard userspace (`libwg-go.so`) from `third-party/wireguard-android`
|
||||||
|
4. Unit tests, adb install (when devices configured)
|
||||||
|
|
||||||
|
No runtime fetch from zx2c4/GitHub during Gradle: sources come from checked-out `third-party/` submodules only.
|
||||||
|
|
||||||
## Trigger a build
|
## Trigger a build
|
||||||
|
|
||||||
1. Open Builder → sign in (default dev user from crash schema seed).
|
1. Open Builder → sign in (default dev user from crash schema seed).
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ See the full proposal: [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVE
|
|||||||
- **WireGuard:** foreground service; jittered poll **1–7 min** → `heartbeat.php` with `type: ra`
|
- **WireGuard:** foreground service; jittered poll **1–7 min** → `heartbeat.php` with `type: ra`
|
||||||
- Heartbeat URL derived from crash upload URL (`…/api/heartbeat.php`)
|
- Heartbeat URL derived from crash upload URL (`…/api/heartbeat.php`)
|
||||||
- Session credentials persisted until `expires_at`; expiry tears down tunnel locally
|
- Session credentials persisted until `expires_at`; expiry tears down tunnel locally
|
||||||
- Optional full userspace WG via `third-party/wireguard-android` (`:tunnel`); otherwise `RemoteAccessVpnService` TUN fallback
|
- Userspace WG via `third-party/wireguard-android` (`:tunnel` / `GoBackend`) in isolated process **`com.foxx.androidcast:vpn`**; main app controls via AIDL (`IVpnService` / `VpnServiceClient`); TUN fallback only if GoBackend fails
|
||||||
|
|
||||||
### Android VPN consent (WireGuard)
|
### Android VPN consent (WireGuard)
|
||||||
|
|
||||||
@@ -148,14 +148,28 @@ Internet UDP :51820 → FE DNAT → BE wg0 :51820
|
|||||||
|
|
||||||
Hostname `ra.apps.f0xx.org` (or similar) should resolve to the FE public IP. Devices use `wg_endpoint` from config/connect payload.
|
Hostname `ra.apps.f0xx.org` (or similar) should resolve to the FE public IP. Devices use `wg_endpoint` from config/connect payload.
|
||||||
|
|
||||||
|
## VPN process (AIDL)
|
||||||
|
|
||||||
|
| Component | Process | Role |
|
||||||
|
|-----------|---------|------|
|
||||||
|
| `VpnServiceClient` | main (`com.foxx.androidcast`) | Binds to `IVpnService`; async apply/tearDown + state callbacks |
|
||||||
|
| `AndroidCastVpnService` | `:vpn` | AIDL stub; worker thread for tunnel ops |
|
||||||
|
| `WireGuardVpnEngine` | `:vpn` | `GoBackend` + TUN fallback |
|
||||||
|
| `RemoteAccessVpnService` | `:vpn` | `VpnService` TUN when GoBackend fails |
|
||||||
|
| `GoBackend$VpnService` | `:vpn` | WireGuard tunnel library VPN entry (manifest merge) |
|
||||||
|
|
||||||
|
Developer settings still uses `VpnService.prepare(Activity)` for the one-time consent dialog (same app UID).
|
||||||
|
|
||||||
|
**Crash capture:** `CrashReporter.install()` registers uncaught handlers in `:vpn` (scenario `vpn_service`, process `com.foxx.androidcast:vpn`). Reports persist to shared app storage and upload via `:crashwatcher`. Lab: `scripts/simulate-vpn-crashes.sh` (debug APK + `MODE=device`, or `MODE=upload` for BE ingest validation).
|
||||||
|
|
||||||
## WireGuard third-party (Android)
|
## WireGuard third-party (Android)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash scripts/init-wireguard-submodule.sh
|
bash scripts/init-third-party-submodules.sh # or ./rebuild.sh (includes init + :tunnel build)
|
||||||
./gradlew :app:assembleDebug # includes :tunnel when submodule present
|
./gradlew :tunnel:assembleDebug :app:assembleDebug
|
||||||
```
|
```
|
||||||
|
|
||||||
Without the submodule, `WireGuardTunnelBridge` uses `RemoteAccessVpnService` (TUN only — sufficient for lab; prefer `:tunnel` for production tunnels).
|
Gradle fails fast if the submodule is missing. VPN runs in `:vpn` (`AndroidCastVpnService` + AIDL); `WireGuardVpnEngine` uses `GoBackend` with TUN fallback in the same process.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
|||||||
6
docs/drafts/20100611_1_net_obfuscation.txt
Normal file
6
docs/drafts/20100611_1_net_obfuscation.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Network traffic obfuscation
|
||||||
|
|
||||||
|
Definitions:
|
||||||
|
- E2E - E2E encryption
|
||||||
|
- P2P - P2P encryption
|
||||||
|
- POLY - Polymorphic ciphers, persistent and temporary keys
|
||||||
12
docs/drafts/20100611_1_net_topology_glossary.txt
Normal file
12
docs/drafts/20100611_1_net_topology_glossary.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
network topology glossary (to be used as a code names within the product):
|
||||||
|
|
||||||
|
1. SIRENE - P2P-alike topology
|
||||||
|
2. SPIDER - MCU-alike topology
|
||||||
|
3. SPIDERWEB - SFU-alike topology
|
||||||
|
4. MINE - mesh-alike topology
|
||||||
|
|
||||||
|
5. SLY - E2E/VPN/hidden/any
|
||||||
|
6. BLUEMOON - Bluetooth-transported any available (mesh, P2P)
|
||||||
|
7. SLOTH - passive
|
||||||
|
|
||||||
|
and the combinations of them: SIRENE SLOTH - slow-to-respond passive P2P networking; SPIDER SLY - hidden non-transparent network activity using MCU-alike topology; MINE BLUEMOON, etc.
|
||||||
17
docs/drafts/20100611_2_av_conversion_and_non_rt.txt
Normal file
17
docs/drafts/20100611_2_av_conversion_and_non_rt.txt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
Audio/Video RT and non-RT
|
||||||
|
linked drafts:
|
||||||
|
- Network obfuscation
|
||||||
|
- Transport layers
|
||||||
|
- Network topology
|
||||||
|
- Paid services
|
||||||
|
- End-user services
|
||||||
|
|
||||||
|
The service (standalone or the current one, depends on max. capacity, bandwidth, storage trade-offs, CPU load) should provide:
|
||||||
|
|
||||||
|
1. RT AV
|
||||||
|
- RT streaming and casting using well-known codecs and transports, including store recordings services for offline / non-RT usage
|
||||||
|
|
||||||
|
2. non-RT AV
|
||||||
|
- non-RT on-demand conversion routines with additional services, like:
|
||||||
|
** reproducible (playable) streams
|
||||||
|
** ability to download/delete streams
|
||||||
@@ -26,7 +26,7 @@ These are **server-side**; not bundled in the mobile APK.
|
|||||||
| **wireguard-tools** (`wg`, `wg-quick`) | GPLv2 (userspace) | BE dynamic peer provisioning (`WireGuardPeerProvisioner.php`) |
|
| **wireguard-tools** (`wg`, `wg-quick`) | GPLv2 (userspace) | BE dynamic peer provisioning (`WireGuardPeerProvisioner.php`) |
|
||||||
| Linux **WireGuard** kernel module | GPLv2 | UDP tunnel termination on BE |
|
| Linux **WireGuard** kernel module | GPLv2 | UDP tunnel termination on BE |
|
||||||
|
|
||||||
Mobile WireGuard mode uses outbound tunnel from device; see mobile app licenses for optional `wireguard-android` (Apache-2.0).
|
Mobile WireGuard mode uses outbound tunnel from device; app embeds `wireguard-android` tunnel (`third-party/wireguard-android`, Apache-2.0) — see app `licenses/combined.html`.
|
||||||
|
|
||||||
## Planned: reverse SSH (RSSH)
|
## Planned: reverse SSH (RSSH)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# wireguard-android tunnel (third-party/wireguard-android); consumed by gradle/wireguard-tunnel/
|
||||||
|
wireguardPackageName=com.wireguard.android
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
# Gradle 8.9 must run on JDK 17–21 (Gentoo default JDK 25 fails with class file version 69).
|
# Gradle 8.9 must run on JDK 17–21 (Gentoo default JDK 25 fails with class file version 69).
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
7
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,7 +0,0 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
@@ -13,13 +13,10 @@ The app loads `libandroidcast_codecs.so` for availability probes and future soft
|
|||||||
|
|
||||||
## Enable native codecs (developer)
|
## Enable native codecs (developer)
|
||||||
|
|
||||||
1. Initialize submodules as needed:
|
1. Initialize all third-party submodules:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule update --init third-party/libvpx
|
bash scripts/init-third-party-submodules.sh
|
||||||
git submodule update --init third-party/opus
|
|
||||||
git submodule update --init third-party/speex
|
|
||||||
git submodule update --init third-party/wireguard-android
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Build static libs per ABI (NDK auto-detected via `scripts/android-ndk.sh`):
|
2. Build static libs per ABI (NDK auto-detected via `scripts/android-ndk.sh`):
|
||||||
|
|||||||
@@ -142,6 +142,9 @@ export ANDROIDCAST_ROOT="$ROOT"
|
|||||||
export ANDROID_NDK_HOME="$(androidcast_find_ndk_root "$ROOT")"
|
export ANDROID_NDK_HOME="$(androidcast_find_ndk_root "$ROOT")"
|
||||||
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
|
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
|
||||||
echo "==> Using NDK: $ANDROID_NDK_HOME"
|
echo "==> Using NDK: $ANDROID_NDK_HOME"
|
||||||
|
|
||||||
|
echo "==> Initializing third-party git submodules (libvpx, opus, speex, wireguard-android)"
|
||||||
|
bash "$ROOT/scripts/init-third-party-submodules.sh"
|
||||||
if androidcast_ccache_wanted 2>/dev/null; then
|
if androidcast_ccache_wanted 2>/dev/null; then
|
||||||
echo "==> Native ccache: enabled (ANDROIDCAST_CCACHE=${ANDROIDCAST_CCACHE:-auto})"
|
echo "==> Native ccache: enabled (ANDROIDCAST_CCACHE=${ANDROIDCAST_CCACHE:-auto})"
|
||||||
else
|
else
|
||||||
@@ -158,15 +161,18 @@ for ARCH_ABI in ${ARCH_ABIS}; do
|
|||||||
LC_ALL=C ./scripts/build-native-codecs.sh "${ARCH_ABI}"
|
LC_ALL=C ./scripts/build-native-codecs.sh "${ARCH_ABI}"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
WG_HEAD="$(git -C "$ROOT/third-party/wireguard-android" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||||
|
echo "==> Building wireguard-android (:tunnel) from third-party/wireguard-android @ $WG_HEAD"
|
||||||
echo "==> Building debug APK (JAVA_HOME=$JAVA_HOME)"
|
echo "==> Building debug APK (JAVA_HOME=$JAVA_HOME)"
|
||||||
# Docker CI on a mounted repo leaves app/.cxx with /opt/android-sdk paths; that breaks local clean/build.
|
# Docker CI on a mounted repo leaves app/.cxx with /opt/android-sdk paths; that breaks local clean/build.
|
||||||
rm -rf "$ROOT/app/.cxx"
|
rm -rf "$ROOT/app/.cxx"
|
||||||
|
rm -rf "$ROOT/gradle/wireguard-tunnel/.cxx" "$ROOT/gradle/wireguard-tunnel/build" 2>/dev/null || true
|
||||||
if [[ -d "$ROOT/app/build" ]] && ! rm -rf "$ROOT/app/build" 2>/dev/null; then
|
if [[ -d "$ROOT/app/build" ]] && ! rm -rf "$ROOT/app/build" 2>/dev/null; then
|
||||||
echo "WARN: app/build is not writable (often root-owned after Docker). Run: sudo rm -rf app/build" >&2
|
echo "WARN: app/build is not writable (often root-owned after Docker). Run: sudo rm -rf app/build" >&2
|
||||||
fi
|
fi
|
||||||
./gradlew --stop
|
./gradlew --stop
|
||||||
./gradlew clean
|
./gradlew clean
|
||||||
./gradlew --build-cache assembleDebug
|
./gradlew --build-cache :tunnel:assembleDebug :app:assembleDebug
|
||||||
|
|
||||||
if [[ "$RUN_UNIT_TESTS" == "1" ]]; then
|
if [[ "$RUN_UNIT_TESTS" == "1" ]]; then
|
||||||
echo "==> Running unit tests (./gradlew testDebugUnitTest)"
|
echo "==> Running unit tests (./gradlew testDebugUnitTest)"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# CMake in ndk/ autolinks when those archives exist (ANDROIDCAST_HAVE_* = 1).
|
# CMake in ndk/ autolinks when those archives exist (ANDROIDCAST_HAVE_* = 1).
|
||||||
#
|
#
|
||||||
# Prerequisites:
|
# Prerequisites:
|
||||||
# git submodule update --init third-party/libvpx
|
# bash scripts/init-third-party-submodules.sh
|
||||||
# Android NDK (auto-detected via scripts/android-ndk.sh)
|
# Android NDK (auto-detected via scripts/android-ndk.sh)
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
@@ -22,7 +22,7 @@ BUILD_DIR="$OUT/libvpx-build"
|
|||||||
|
|
||||||
if [[ ! -d "$VPX_SRC_REAL" ]]; then
|
if [[ ! -d "$VPX_SRC_REAL" ]]; then
|
||||||
echo "ERROR: $VPX_SRC_REAL missing. Run:"
|
echo "ERROR: $VPX_SRC_REAL missing. Run:"
|
||||||
echo " git submodule update --init third-party/libvpx"
|
echo " bash scripts/init-third-party-submodules.sh"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ androidcast_ndk_configure_host() {
|
|||||||
build_opus_for_abi() {
|
build_opus_for_abi() {
|
||||||
local opus_src="$ROOT/third-party/opus"
|
local opus_src="$ROOT/third-party/opus"
|
||||||
if [[ ! -f "$opus_src/CMakeLists.txt" ]]; then
|
if [[ ! -f "$opus_src/CMakeLists.txt" ]]; then
|
||||||
echo "Skip libopus: $opus_src missing (git submodule update --init third-party/opus)"
|
echo "Skip libopus: $opus_src missing (bash scripts/init-third-party-submodules.sh)"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
local opus_build="$BUILD_DIR/opus-cmake"
|
local opus_build="$BUILD_DIR/opus-cmake"
|
||||||
@@ -284,7 +284,7 @@ build_opus_for_abi() {
|
|||||||
build_speex_for_abi() {
|
build_speex_for_abi() {
|
||||||
local speex_src_real="$ROOT/third-party/speex"
|
local speex_src_real="$ROOT/third-party/speex"
|
||||||
if [[ ! -d "$speex_src_real" ]]; then
|
if [[ ! -d "$speex_src_real" ]]; then
|
||||||
echo "Skip libspeex: $speex_src_real missing (git submodule update --init third-party/speex)"
|
echo "Skip libspeex: $speex_src_real missing (bash scripts/init-third-party-submodules.sh)"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
local speex_src="$speex_src_real"
|
local speex_src="$speex_src_real"
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ if [[ -n "${ANDROID_SDK_ROOT:-}" ]]; then
|
|||||||
printf 'sdk.dir=%s\n' "${ANDROID_SDK_ROOT//\\/\\\\}" > "$LOCAL_PROPS"
|
printf 'sdk.dir=%s\n' "${ANDROID_SDK_ROOT//\\/\\\\}" > "$LOCAL_PROPS"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "==> Phase: third-party submodules (libvpx, opus, speex, wireguard-android)"
|
||||||
|
bash "$ROOT/scripts/init-third-party-submodules.sh"
|
||||||
|
|
||||||
if [[ "$RUN_NATIVE" == "1" && "$RUN_APK" == "1" ]]; then
|
if [[ "$RUN_NATIVE" == "1" && "$RUN_APK" == "1" ]]; then
|
||||||
echo "==> Phase: native codecs"
|
echo "==> Phase: native codecs"
|
||||||
ABIS="${ANDROIDCAST_CI_ABIS:-armeabi-v7a arm64-v8a x86_64}"
|
ABIS="${ANDROIDCAST_CI_ABIS:-armeabi-v7a arm64-v8a x86_64}"
|
||||||
@@ -75,17 +78,21 @@ fi
|
|||||||
|
|
||||||
# Avoid host-specific CMake cache paths when running in Docker.
|
# Avoid host-specific CMake cache paths when running in Docker.
|
||||||
rm -rf "$ROOT/app/.cxx" "$ROOT/app/build/.cxx" "$ROOT/app/build/intermediates/cxx"
|
rm -rf "$ROOT/app/.cxx" "$ROOT/app/build/.cxx" "$ROOT/app/build/intermediates/cxx"
|
||||||
|
rm -rf "$ROOT/gradle/wireguard-tunnel/.cxx" "$ROOT/gradle/wireguard-tunnel/build" 2>/dev/null || true
|
||||||
|
|
||||||
|
WG_HEAD="$(git -C "$ROOT/third-party/wireguard-android" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||||
|
echo "==> Phase: wireguard-android (:tunnel) third-party/wireguard-android @ ${WG_HEAD}"
|
||||||
|
|
||||||
if [[ "$RUN_APK" == "1" ]]; then
|
if [[ "$RUN_APK" == "1" ]]; then
|
||||||
echo "==> Phase: gradle ${GRADLE_TASK}"
|
echo "==> Phase: gradle :tunnel + ${GRADLE_TASK}"
|
||||||
if [[ "$RUN_UNIT_TESTS" == "1" ]]; then
|
if [[ "$RUN_UNIT_TESTS" == "1" ]]; then
|
||||||
./gradlew --no-daemon "${GRADLE_JAVA_OPT[@]}" clean "${GRADLE_TASK}" testDebugUnitTest
|
./gradlew --no-daemon "${GRADLE_JAVA_OPT[@]}" clean :tunnel:assembleDebug "${GRADLE_TASK}" testDebugUnitTest
|
||||||
else
|
else
|
||||||
./gradlew --no-daemon "${GRADLE_JAVA_OPT[@]}" clean "${GRADLE_TASK}"
|
./gradlew --no-daemon "${GRADLE_JAVA_OPT[@]}" clean :tunnel:assembleDebug "${GRADLE_TASK}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "==> Phase: gradle tests-only"
|
echo "==> Phase: gradle tests-only (:tunnel + testDebugUnitTest)"
|
||||||
./gradlew --no-daemon "${GRADLE_JAVA_OPT[@]}" testDebugUnitTest
|
./gradlew --no-daemon "${GRADLE_JAVA_OPT[@]}" :tunnel:assembleDebug testDebugUnitTest
|
||||||
fi
|
fi
|
||||||
|
|
||||||
APK=""
|
APK=""
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ source "$ROOT/scripts/android-ndk.sh"
|
|||||||
export ANDROID_NDK_HOME="$(androidcast_find_ndk_root "$ROOT")"
|
export ANDROID_NDK_HOME="$(androidcast_find_ndk_root "$ROOT")"
|
||||||
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
|
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
|
||||||
|
|
||||||
if [[ -f .gitmodules ]]; then
|
echo "==> Initializing third-party git submodules"
|
||||||
git submodule update --init --recursive
|
bash "$ROOT/scripts/init-third-party-submodules.sh"
|
||||||
fi
|
|
||||||
|
|
||||||
ABIS="${ANDROIDCAST_CI_ABIS:-armeabi-v7a arm64-v8a x86_64}"
|
ABIS="${ANDROIDCAST_CI_ABIS:-armeabi-v7a arm64-v8a x86_64}"
|
||||||
echo "==> Building libvpx for: $ABIS"
|
echo "==> Building libvpx for: $ABIS"
|
||||||
@@ -26,7 +25,7 @@ for abi in $ABIS; do
|
|||||||
./scripts/build-native-codecs.sh "$abi"
|
./scripts/build-native-codecs.sh "$abi"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "==> Gradle assembleDebug + unit tests"
|
echo "==> Gradle :tunnel + assembleDebug + unit tests"
|
||||||
./gradlew --no-daemon clean assembleDebug testDebugUnitTest
|
./gradlew --no-daemon clean :tunnel:assembleDebug assembleDebug testDebugUnitTest
|
||||||
|
|
||||||
echo "==> Done: $ROOT/app/build/outputs/apk/debug/app-debug.apk"
|
echo "==> Done: $ROOT/app/build/outputs/apk/debug/app-debug.apk"
|
||||||
|
|||||||
110
scripts/init-third-party-submodules.sh
Executable file
110
scripts/init-third-party-submodules.sh
Executable file
@@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Sync and initialize all third-party/ git submodules from .gitmodules (recursive nested).
|
||||||
|
# Used by rebuild.sh, ci-build.sh, and deploy paths — no network except submodule fetch/update.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
ONLY_PATH=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--only)
|
||||||
|
ONLY_PATH="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
echo "Usage: $0 [--only third-party/wireguard-android]"
|
||||||
|
echo " Initializes every path in .gitmodules under third-party/ (recursive)."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "error: unknown argument: $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -f .gitmodules ]]; then
|
||||||
|
echo "error: .gitmodules not found in $ROOT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t SUB_PATHS < <(git config -f .gitmodules --get-regexp '^submodule\..*\.path$' \
|
||||||
|
| awk '{ print $2 }' | grep '^third-party/' | sort -u)
|
||||||
|
|
||||||
|
if [[ ${#SUB_PATHS[@]} -eq 0 ]]; then
|
||||||
|
echo "error: no third-party paths in .gitmodules" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$ONLY_PATH" ]]; then
|
||||||
|
SUB_PATHS=("$ONLY_PATH")
|
||||||
|
fi
|
||||||
|
|
||||||
|
ensure_gitlink() {
|
||||||
|
local path="$1"
|
||||||
|
local name="$path"
|
||||||
|
if git cat-file -e "HEAD:${path}" 2>/dev/null \
|
||||||
|
|| git ls-files --stage "$path" | grep -q '^160000'; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local url
|
||||||
|
url="$(git config -f .gitmodules --get "submodule.${name}.url" || true)"
|
||||||
|
if [[ -z "$url" ]]; then
|
||||||
|
echo "error: $path listed in .gitmodules but no gitlink in tree and no url" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local sha
|
||||||
|
sha="$(git ls-remote "$url" HEAD | awk 'NR==1 {print $1}')"
|
||||||
|
if [[ -z "$sha" ]]; then
|
||||||
|
echo "error: could not resolve HEAD for $url (needed to register missing gitlink)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "WARN: registering missing gitlink for $path at $sha (commit this gitlink)" >&2
|
||||||
|
git update-index --add --cacheinfo 160000,"$sha","$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_path() {
|
||||||
|
local path="$1"
|
||||||
|
case "$path" in
|
||||||
|
third-party/wireguard-android)
|
||||||
|
[[ -d "$path/tunnel/src/main/java" ]] \
|
||||||
|
&& [[ -f "$path/tunnel/tools/wireguard-tools/src/config.c" ]] \
|
||||||
|
&& [[ -f "$path/tunnel/tools/elf-cleaner/elf-cleaner.cpp" || -f "$path/tunnel/tools/elf-cleaner/main.cpp" ]]
|
||||||
|
;;
|
||||||
|
third-party/libvpx)
|
||||||
|
[[ -d "$path" ]]
|
||||||
|
;;
|
||||||
|
third-party/opus|third-party/speex)
|
||||||
|
[[ -f "$path/configure" || -f "$path/CMakeLists.txt" || -d "$path/include" ]]
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
[[ -d "$path" ]]
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "==> third-party submodules: sync + update --init --recursive"
|
||||||
|
for path in "${SUB_PATHS[@]}"; do
|
||||||
|
if ! grep -q "$path" .gitmodules 2>/dev/null; then
|
||||||
|
echo "error: $path not in .gitmodules" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ensure_gitlink "$path"
|
||||||
|
git submodule sync "$path"
|
||||||
|
git submodule update --init --recursive "$path"
|
||||||
|
if ! validate_path "$path"; then
|
||||||
|
echo "error: $path incomplete after submodule update (nested submodules missing?)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
short="$(git -C "$path" rev-parse --short HEAD 2>/dev/null || echo '?')"
|
||||||
|
echo " $path @ $short"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -d third-party/wireguard-android/tunnel ]]; then
|
||||||
|
wg_tools="$(git -C third-party/wireguard-android/tunnel/tools/wireguard-tools rev-parse --short HEAD 2>/dev/null || echo '?')"
|
||||||
|
echo " third-party/wireguard-android/tunnel/tools/wireguard-tools @ $wg_tools"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> third-party submodules ready"
|
||||||
@@ -1,36 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Initialize wireguard-android submodule (third-party/wireguard-android in .gitmodules).
|
# Back-compat wrapper — wireguard-android is one of the third-party submodules.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
cd "$ROOT"
|
exec "$ROOT/scripts/init-third-party-submodules.sh" --only third-party/wireguard-android
|
||||||
|
|
||||||
SUB_PATH="third-party/wireguard-android"
|
|
||||||
SUB_NAME="third-party/wireguard-android"
|
|
||||||
|
|
||||||
if ! grep -q "$SUB_PATH" .gitmodules 2>/dev/null; then
|
|
||||||
echo "error: .gitmodules missing $SUB_PATH" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# .gitmodules without a gitlink in the tree breaks plain `git submodule update`.
|
|
||||||
if ! git cat-file -e "HEAD:$SUB_PATH" 2>/dev/null \
|
|
||||||
&& ! git ls-files --stage "$SUB_PATH" | grep -q '^160000'; then
|
|
||||||
url="$(git config -f .gitmodules --get "submodule.$SUB_NAME.url" || true)"
|
|
||||||
if [ -z "$url" ]; then
|
|
||||||
echo "error: no url for submodule.$SUB_NAME in .gitmodules" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sha="$(git ls-remote "$url" HEAD | awk 'NR==1 {print $1}')"
|
|
||||||
if [ -z "$sha" ]; then
|
|
||||||
echo "error: could not resolve HEAD for $url" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "registering missing gitlink for $SUB_PATH at $sha"
|
|
||||||
git update-index --add --cacheinfo 160000,"$sha","$SUB_PATH"
|
|
||||||
echo "warning: gitlink was missing from the tree — commit third-party/wireguard-android after init" >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
git submodule sync "$SUB_PATH"
|
|
||||||
git submodule update --init --recursive "$SUB_PATH"
|
|
||||||
echo "wireguard-android ready under $SUB_PATH"
|
|
||||||
echo "Gradle includes :tunnel when $SUB_PATH/tunnel exists."
|
|
||||||
|
|||||||
196
scripts/simulate-vpn-crashes.sh
Executable file
196
scripts/simulate-vpn-crashes.sh
Executable file
@@ -0,0 +1,196 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# VPN crash simulation lab — device (adb) and/or BE upload verification.
|
||||||
|
#
|
||||||
|
# Modes:
|
||||||
|
# MODE=device — trigger VpnCrashSimulator in :vpn via adb (debug APK)
|
||||||
|
# MODE=upload — POST synthetic vpn-process crash JSON to BE upload API
|
||||||
|
# MODE=both — device then verify BE (default when adb device present)
|
||||||
|
#
|
||||||
|
# Env:
|
||||||
|
# COUNT=70
|
||||||
|
# BASE=https://apps.f0xx.org/app/androidcast_project/crashes
|
||||||
|
# RUN_ID=YYYYMMDD_HHMMSS (batch tag for Issues search)
|
||||||
|
# PACKAGE=com.foxx.androidcast
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
COUNT="${COUNT:-70}"
|
||||||
|
BASE="${BASE:-https://apps.f0xx.org/app/androidcast_project/crashes}"
|
||||||
|
UPLOAD_URL="$BASE/api/upload.php"
|
||||||
|
PACKAGE="${PACKAGE:-com.foxx.androidcast}"
|
||||||
|
RUN_ID="${RUN_ID:-$(date -u +%Y%m%d_%H%M%S)}"
|
||||||
|
MODE="${MODE:-auto}"
|
||||||
|
JAR="${JAR:-/tmp/ac_vpn_crash_cookies.txt}"
|
||||||
|
DELAY_MS="${DELAY_MS:-80}"
|
||||||
|
VERIFY_LOGIN="${VERIFY_LOGIN:-1}"
|
||||||
|
|
||||||
|
log() { printf '[vpn-crash-sim] %s\n' "$*"; }
|
||||||
|
|
||||||
|
has_device() {
|
||||||
|
adb devices 2>/dev/null | awk 'NR>1 && $2=="device" { found=1 } END { exit !found }'
|
||||||
|
}
|
||||||
|
|
||||||
|
simulate_device() {
|
||||||
|
local n="$1"
|
||||||
|
local i seq
|
||||||
|
log "device: triggering $n VPN-process crash recordings via $PACKAGE"
|
||||||
|
for ((i = 1; i <= n; i++)); do
|
||||||
|
seq="$i"
|
||||||
|
adb shell am start-service \
|
||||||
|
-n "$PACKAGE/com.foxx.androidcast.vpn.AndroidCastVpnService" \
|
||||||
|
-a com.foxx.androidcast.vpn.SIMULATE_CRASH \
|
||||||
|
--ei sim_seq "$seq" >/dev/null 2>&1 \
|
||||||
|
|| adb shell am startservice \
|
||||||
|
-n "$PACKAGE/com.foxx.androidcast.vpn.AndroidCastVpnService" \
|
||||||
|
-a com.foxx.androidcast.vpn.SIMULATE_CRASH \
|
||||||
|
--ei sim_seq "$seq" >/dev/null 2>&1 \
|
||||||
|
|| true
|
||||||
|
if [[ "$DELAY_MS" -gt 0 ]]; then
|
||||||
|
sleep "$(awk "BEGIN { print $DELAY_MS / 1000 }")"
|
||||||
|
fi
|
||||||
|
if (( i % 10 == 0 )); then
|
||||||
|
log " device progress $i/$n"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
log "device: done — waiting for crash watcher upload sweep"
|
||||||
|
sleep 15
|
||||||
|
}
|
||||||
|
|
||||||
|
upload_one() {
|
||||||
|
local seq="$1"
|
||||||
|
local report_id="vpn_sim_${RUN_ID}_$(printf '%03d' "$seq")"
|
||||||
|
local ms
|
||||||
|
ms="$(date +%s)000"
|
||||||
|
local fp
|
||||||
|
fp="$(printf 'vpn_sim_%s_%03d' "$RUN_ID" "$seq" | sha256sum | awk '{print $1}')"
|
||||||
|
local body
|
||||||
|
body="$(cat <<EOF
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"report_id": "$report_id",
|
||||||
|
"generated_at_epoch_ms": $ms,
|
||||||
|
"crash_type": "java",
|
||||||
|
"scenario": "vpn_service",
|
||||||
|
"process": "com.foxx.androidcast:vpn",
|
||||||
|
"fingerprint": "$fp",
|
||||||
|
"tags": ["vpn_sim", "$RUN_ID"],
|
||||||
|
"device": {
|
||||||
|
"manufacturer": "SimLab",
|
||||||
|
"brand": "AndroidCast",
|
||||||
|
"model": "VpnCrashSim",
|
||||||
|
"device": "vpn_sim",
|
||||||
|
"product": "vpn_sim",
|
||||||
|
"sdk_int": 34,
|
||||||
|
"release": "14",
|
||||||
|
"abis": ["arm64-v8a"]
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"package": "$PACKAGE",
|
||||||
|
"version_name": "0.1.0",
|
||||||
|
"version_code": 100
|
||||||
|
},
|
||||||
|
"build": { "debug": true, "git_commit": "vpn_sim" },
|
||||||
|
"runtime_context": { "vpn_sim_seq": $seq, "run_id": "$RUN_ID" },
|
||||||
|
"java": {
|
||||||
|
"thread": "androidcast-vpn-worker",
|
||||||
|
"exception": "java.lang.RuntimeException",
|
||||||
|
"message": "VpnCrashSimulation seq=$seq proc=com.foxx.androidcast:vpn",
|
||||||
|
"stack_frames": [
|
||||||
|
"com.foxx.androidcast.vpn.WireGuardVpnEngine.apply(WireGuardVpnEngine.java:40)",
|
||||||
|
"com.foxx.androidcast.vpn.AndroidCastVpnService\$1.applyWireGuardConfig(AndroidCastVpnService.java:60)",
|
||||||
|
"com.foxx.androidcast.vpn.AndroidCastVpnService.onStartCommand(AndroidCastVpnService.java:95)",
|
||||||
|
"android.app.ActivityThread.handleServiceArgs(ActivityThread.java:5001)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
curl -sk -w '\n%{http_code}' -H 'Content-Type: application/json' --data-binary "$body" "$UPLOAD_URL"
|
||||||
|
}
|
||||||
|
|
||||||
|
simulate_upload() {
|
||||||
|
local n="$1"
|
||||||
|
local ok=0 fail=0 dup=0
|
||||||
|
log "upload: posting $n vpn-process crash reports to $UPLOAD_URL (run_id=$RUN_ID)"
|
||||||
|
local i resp code
|
||||||
|
for ((i = 1; i <= n; i++)); do
|
||||||
|
resp="$(upload_one "$i")"
|
||||||
|
code="$(echo "$resp" | tail -n1)"
|
||||||
|
case "$code" in
|
||||||
|
200) ((ok++)) || true ;;
|
||||||
|
409) ((dup++)) || true ;;
|
||||||
|
*) ((fail++)) || true; log " FAIL seq=$i HTTP $code" ;;
|
||||||
|
esac
|
||||||
|
if (( i % 10 == 0 )); then
|
||||||
|
log " upload progress $i/$n (ok=$ok dup=$dup fail=$fail)"
|
||||||
|
fi
|
||||||
|
if [[ "$DELAY_MS" -gt 0 ]]; then
|
||||||
|
sleep "$(awk "BEGIN { print $DELAY_MS / 1000 }")"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
log "upload: ok=$ok duplicate=$dup failed=$fail / $n"
|
||||||
|
[[ "$fail" -eq 0 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_be() {
|
||||||
|
if [[ "$VERIFY_LOGIN" != "1" ]]; then
|
||||||
|
log "verify: skipped (VERIFY_LOGIN=$VERIFY_LOGIN)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log "verify: login + search Issues for run_id=$RUN_ID"
|
||||||
|
curl -sk -c "$JAR" -b "$JAR" -L -X POST "$BASE/login" \
|
||||||
|
-d 'username=admin&password=admin' -o /dev/null || true
|
||||||
|
local json
|
||||||
|
local q="${QUERY:-$RUN_ID}"
|
||||||
|
json="$(curl -sk -b "$JAR" "$BASE/api/reports.php?q=${q}&per_page=200")"
|
||||||
|
local cnt
|
||||||
|
cnt="$(echo "$json" | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
items=d.get('items') or []
|
||||||
|
print(len(items))
|
||||||
|
" 2>/dev/null || echo 0)"
|
||||||
|
log "verify: BE reports matching q=$q → $cnt (expected up to $COUNT)"
|
||||||
|
if [[ "$cnt" -lt 1 ]]; then
|
||||||
|
log "verify: WARN — Issues API returned 0 (login cookie may be invalid in automation)"
|
||||||
|
log "verify: open $BASE/?view=reports&q=$q in browser when logged in"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "$json" | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
for it in (d.get('items') or [])[:3]:
|
||||||
|
p=it.get('payload') or {}
|
||||||
|
j=p.get('java') or {}
|
||||||
|
print(' sample:', it.get('report_id'), 'process=', p.get('process'), 'frames=', len(j.get('stack_frames') or []))
|
||||||
|
" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$MODE" == "auto" ]]; then
|
||||||
|
if has_device; then
|
||||||
|
MODE=both
|
||||||
|
else
|
||||||
|
MODE=upload
|
||||||
|
log "no adb device — using upload-only mode"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
device)
|
||||||
|
simulate_device "$COUNT"
|
||||||
|
;;
|
||||||
|
upload)
|
||||||
|
simulate_upload "$COUNT"
|
||||||
|
verify_be || true
|
||||||
|
;;
|
||||||
|
both)
|
||||||
|
simulate_device "$COUNT"
|
||||||
|
QUERY="${QUERY:-VpnCrashSimulation}" verify_be || true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unknown MODE=$MODE (device|upload|both)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
log "complete run_id=$RUN_ID — Issues: $BASE/?view=reports&q=$RUN_ID"
|
||||||
@@ -21,8 +21,13 @@ include ':app'
|
|||||||
include ':session-studio'
|
include ':session-studio'
|
||||||
project(':session-studio').projectDir = file('desktop/session-studio')
|
project(':session-studio').projectDir = file('desktop/session-studio')
|
||||||
|
|
||||||
def wgTunnelDir = file('third-party/wireguard-android/tunnel')
|
def wgTunnelSrc = file('third-party/wireguard-android/tunnel')
|
||||||
if (wgTunnelDir.exists()) {
|
if (!wgTunnelSrc.isDirectory()) {
|
||||||
include ':tunnel'
|
throw new GradleException(
|
||||||
project(':tunnel').projectDir = wgTunnelDir
|
'Missing third-party/wireguard-android/tunnel — run:\n' +
|
||||||
|
' git submodule update --init --recursive third-party/wireguard-android\n' +
|
||||||
|
' bash scripts/init-wireguard-submodule.sh'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
include ':tunnel'
|
||||||
|
project(':tunnel').projectDir = file('gradle/wireguard-tunnel')
|
||||||
|
|||||||
14
third-party/README.md
vendored
14
third-party/README.md
vendored
@@ -1,19 +1,21 @@
|
|||||||
# Third-party vendored dependencies
|
# Third-party vendored dependencies
|
||||||
|
|
||||||
## wireguard-android (git submodule)
|
## wireguard-android (required git submodule)
|
||||||
|
|
||||||
Listed in `.gitmodules` like libvpx/opus/speex. Initialize after clone:
|
Listed in `.gitmodules` like libvpx/opus/speex. **Required for Gradle** — `:tunnel` is always included. Initialize after clone:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule update --init third-party/wireguard-android
|
bash scripts/init-third-party-submodules.sh
|
||||||
# or:
|
# wireguard only:
|
||||||
bash scripts/init-wireguard-submodule.sh
|
bash scripts/init-wireguard-submodule.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Build with the `:tunnel` module (auto-included when present):
|
`rebuild.sh` and `scripts/ci-build.sh` run the init script automatically.
|
||||||
|
|
||||||
|
Build links the `:tunnel` module via `gradle/wireguard-tunnel/` (sources from this submodule):
|
||||||
|
|
||||||
- Submodule path: `third-party/wireguard-android/`
|
- Submodule path: `third-party/wireguard-android/`
|
||||||
- Gradle module: `third-party/wireguard-android/tunnel`
|
- Gradle module: `third-party/wireguard-android/tunnel`
|
||||||
- License: MIT / Apache-2.0 (see upstream `COPYING`)
|
- License: MIT / Apache-2.0 (see upstream `COPYING`)
|
||||||
|
|
||||||
`WireGuardTunnelBridge` loads `com.wireguard.android.backend.GoBackend` via reflection when the module is on the classpath.
|
`WireGuardVpnEngine` (in `:vpn` process) links `GoBackend` from `:tunnel` (native `libwg-go.so` built from this tree); main app uses `VpnServiceClient` AIDL.
|
||||||
|
|||||||
Reference in New Issue
Block a user