mirror of
git://f0xx.org/android_cast
synced 2026-07-29 04:57:40 +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
|
||||
.gradle/
|
||||
gradle/
|
||||
/local.properties
|
||||
/.idea/
|
||||
/build/
|
||||
|
||||
@@ -110,6 +110,12 @@ android {
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += '/META-INF/versions/9/OSGI-INF/MANIFEST.MF'
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
@@ -133,7 +139,5 @@ dependencies {
|
||||
|
||||
implementation 'com.github.mwiede:jsch:0.2.21'
|
||||
|
||||
if (project.findProject(':tunnel') != null) {
|
||||
implementation project(':tunnel')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?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.REQUEST_INSTALL_PACKAGES" />
|
||||
@@ -133,9 +134,16 @@
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="connectedDevice" />
|
||||
|
||||
<!-- VPN stack: isolated process com.foxx.androidcast:vpn (AIDL via AndroidCastVpnService). -->
|
||||
<service
|
||||
android:name=".remoteaccess.RemoteAccessVpnService"
|
||||
android:name=".vpn.AndroidCastVpnService"
|
||||
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:foregroundServiceType="connectedDevice">
|
||||
<intent-filter>
|
||||
@@ -143,6 +151,17 @@
|
||||
</intent-filter>
|
||||
</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
|
||||
android:name=".TrayNotificationReceiver"
|
||||
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>
|
||||
</ul>
|
||||
|
||||
<h2>WireGuard tunnel module (optional Gradle module)</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>
|
||||
<h2>WireGuard tunnel module (<code>third-party/wireguard-android</code>)</h2>
|
||||
<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
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
CrashReporter.install(this);
|
||||
if (!CrashReporter.isMainProcess(this, CrashReporter.currentProcessName())) {
|
||||
return;
|
||||
}
|
||||
CodecPriorityCatalog.init(this);
|
||||
CastLocaleHelper.applyStoredLocale(this);
|
||||
CastThemeHelper.applyNightMode(this);
|
||||
CrashReporter.install(this);
|
||||
ReceiverAvRuntime.reload(this);
|
||||
CastTransportFactory.init(this);
|
||||
WiredDisplayMonitor.getInstance(this).ensureRegistered();
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
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.content.Context;
|
||||
import android.util.Log;
|
||||
@@ -21,19 +10,29 @@ import org.json.JSONObject;
|
||||
|
||||
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 {
|
||||
private static final String TAG = "CrashReporter";
|
||||
|
||||
private CrashReporter() {}
|
||||
|
||||
/** Install handlers appropriate for the current process. */
|
||||
public static void install(Context context) {
|
||||
Context app = context.getApplicationContext();
|
||||
if (!isMainProcess(app)) {
|
||||
return;
|
||||
String proc = currentProcessName();
|
||||
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);
|
||||
final Thread.UncaughtExceptionHandler previous =
|
||||
@@ -55,6 +54,25 @@ public final class CrashReporter {
|
||||
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) {
|
||||
if (!CrashSettingsStore.load(context).enabled) {
|
||||
return;
|
||||
@@ -95,7 +113,22 @@ public final class CrashReporter {
|
||||
CrashWatcherService.requestImmediateSweep(context, true);
|
||||
}
|
||||
|
||||
private static boolean isMainProcess(Context context) {
|
||||
return context.getPackageName().equals(Application.getProcessName());
|
||||
public static boolean isMainProcess(Context context, String procName) {
|
||||
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() {
|
||||
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()) {
|
||||
return "cast_session";
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.foxx.androidcast.AppPreferences;
|
||||
import com.foxx.androidcast.vpn.VpnServiceClient;
|
||||
|
||||
/** Start/stop remote access foreground polling + VPN based on developer mode. */
|
||||
public final class RemoteAccessCoordinator {
|
||||
@@ -17,7 +18,7 @@ public final class RemoteAccessCoordinator {
|
||||
Log.i(TAG, "disabling remote access");
|
||||
RemoteAccessService.sendDisableOnce(app);
|
||||
RemoteAccessService.stop(app);
|
||||
RemoteAccessVpnService.stop(app);
|
||||
VpnServiceClient.get(app).tearDown();
|
||||
AppPreferences.clearDevRemoteAccessSession(app);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.foxx.androidcast.AppPreferences;
|
||||
import com.foxx.androidcast.DeveloperSettingsActivity;
|
||||
import com.foxx.androidcast.DeviceInfo;
|
||||
import com.foxx.androidcast.R;
|
||||
import com.foxx.androidcast.vpn.VpnServiceClient;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
@@ -185,21 +186,30 @@ public final class RemoteAccessService extends Service {
|
||||
if (creds == null) {
|
||||
return;
|
||||
}
|
||||
boolean ok = false;
|
||||
if ("wireguard".equals(tunnel)) {
|
||||
String wgConfig = WireGuardConfigBuilder.fromConnectCredentials(creds);
|
||||
if (!wgConfig.isEmpty()) {
|
||||
ok = WireGuardTunnelBridge.applyConfig(this, wgConfig);
|
||||
}
|
||||
} else if ("ssh_reverse".equals(tunnel)) {
|
||||
ok = ReverseSshTunnelBridge.applyConfig(this, creds);
|
||||
} else {
|
||||
Log.w(TAG, "unsupported tunnel in connect: " + tunnel);
|
||||
if (wgConfig.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (ok) {
|
||||
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)) {
|
||||
if (ReverseSshTunnelBridge.applyConfig(this, creds)) {
|
||||
updateNotificationConnected(sessionId, tunnel);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "unsupported tunnel in connect: " + tunnel);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateNotificationConnected(String sessionId, String tunnel) {
|
||||
|
||||
@@ -2,12 +2,14 @@ package com.foxx.androidcast.remoteaccess;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.foxx.androidcast.vpn.VpnServiceClient;
|
||||
|
||||
/** Tear down all remote-access transports (WireGuard + RSSH). */
|
||||
public final class RemoteAccessTunnelHelper {
|
||||
private RemoteAccessTunnelHelper() {}
|
||||
|
||||
public static void tearDownAll(Context context) {
|
||||
WireGuardTunnelBridge.tearDown(context);
|
||||
VpnServiceClient.get(context).tearDown();
|
||||
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.NotificationChannel;
|
||||
@@ -18,14 +18,15 @@ import com.foxx.androidcast.DeveloperSettingsActivity;
|
||||
import com.foxx.androidcast.R;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.regex.Matcher;
|
||||
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 {
|
||||
private static final String TAG = "RemoteAccessVPN";
|
||||
public static final String ACTION_UP = "com.foxx.androidcast.remoteaccess.VPN_UP";
|
||||
public static final String ACTION_DOWN = "com.foxx.androidcast.remoteaccess.VPN_DOWN";
|
||||
public static final String ACTION_UP = "com.foxx.androidcast.vpn.VPN_UP";
|
||||
public static final String ACTION_DOWN = "com.foxx.androidcast.vpn.VPN_DOWN";
|
||||
public static final String EXTRA_WG_CONFIG = "wg_config";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/** Routes from AllowedIPs= lines (split tunnel — typically server wg /32). */
|
||||
private static String[] parseAllowedRoutes(String wgConfig) {
|
||||
if (wgConfig == null) {
|
||||
return new String[0];
|
||||
@@ -118,7 +118,7 @@ public final class RemoteAccessVpnService extends VpnService {
|
||||
return new String[0];
|
||||
}
|
||||
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) {
|
||||
if (!p.isEmpty()) {
|
||||
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`.
|
||||
|
||||
## 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
|
||||
|
||||
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`
|
||||
- Heartbeat URL derived from crash upload URL (`…/api/heartbeat.php`)
|
||||
- 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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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)
|
||||
|
||||
```bash
|
||||
bash scripts/init-wireguard-submodule.sh
|
||||
./gradlew :app:assembleDebug # includes :tunnel when submodule present
|
||||
bash scripts/init-third-party-submodules.sh # or ./rebuild.sh (includes init + :tunnel build)
|
||||
./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
|
||||
|
||||
|
||||
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`) |
|
||||
| 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)
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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.nonTransitiveRClass=true
|
||||
# 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)
|
||||
|
||||
1. Initialize submodules as needed:
|
||||
1. Initialize all third-party submodules:
|
||||
|
||||
```bash
|
||||
git submodule update --init third-party/libvpx
|
||||
git submodule update --init third-party/opus
|
||||
git submodule update --init third-party/speex
|
||||
git submodule update --init third-party/wireguard-android
|
||||
bash scripts/init-third-party-submodules.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_ROOT="$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
|
||||
echo "==> Native ccache: enabled (ANDROIDCAST_CCACHE=${ANDROIDCAST_CCACHE:-auto})"
|
||||
else
|
||||
@@ -158,15 +161,18 @@ for ARCH_ABI in ${ARCH_ABIS}; do
|
||||
LC_ALL=C ./scripts/build-native-codecs.sh "${ARCH_ABI}"
|
||||
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)"
|
||||
# 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/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
|
||||
echo "WARN: app/build is not writable (often root-owned after Docker). Run: sudo rm -rf app/build" >&2
|
||||
fi
|
||||
./gradlew --stop
|
||||
./gradlew clean
|
||||
./gradlew --build-cache assembleDebug
|
||||
./gradlew --build-cache :tunnel:assembleDebug :app:assembleDebug
|
||||
|
||||
if [[ "$RUN_UNIT_TESTS" == "1" ]]; then
|
||||
echo "==> Running unit tests (./gradlew testDebugUnitTest)"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# CMake in ndk/ autolinks when those archives exist (ANDROIDCAST_HAVE_* = 1).
|
||||
#
|
||||
# Prerequisites:
|
||||
# git submodule update --init third-party/libvpx
|
||||
# bash scripts/init-third-party-submodules.sh
|
||||
# Android NDK (auto-detected via scripts/android-ndk.sh)
|
||||
#
|
||||
# Usage:
|
||||
@@ -22,7 +22,7 @@ BUILD_DIR="$OUT/libvpx-build"
|
||||
|
||||
if [[ ! -d "$VPX_SRC_REAL" ]]; then
|
||||
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
|
||||
fi
|
||||
|
||||
@@ -253,7 +253,7 @@ androidcast_ndk_configure_host() {
|
||||
build_opus_for_abi() {
|
||||
local opus_src="$ROOT/third-party/opus"
|
||||
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
|
||||
fi
|
||||
local opus_build="$BUILD_DIR/opus-cmake"
|
||||
@@ -284,7 +284,7 @@ build_opus_for_abi() {
|
||||
build_speex_for_abi() {
|
||||
local speex_src_real="$ROOT/third-party/speex"
|
||||
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
|
||||
fi
|
||||
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"
|
||||
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
|
||||
echo "==> Phase: native codecs"
|
||||
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.
|
||||
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
|
||||
echo "==> Phase: gradle ${GRADLE_TASK}"
|
||||
echo "==> Phase: gradle :tunnel + ${GRADLE_TASK}"
|
||||
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
|
||||
./gradlew --no-daemon "${GRADLE_JAVA_OPT[@]}" clean "${GRADLE_TASK}"
|
||||
./gradlew --no-daemon "${GRADLE_JAVA_OPT[@]}" clean :tunnel:assembleDebug "${GRADLE_TASK}"
|
||||
fi
|
||||
else
|
||||
echo "==> Phase: gradle tests-only"
|
||||
./gradlew --no-daemon "${GRADLE_JAVA_OPT[@]}" testDebugUnitTest
|
||||
echo "==> Phase: gradle tests-only (:tunnel + testDebugUnitTest)"
|
||||
./gradlew --no-daemon "${GRADLE_JAVA_OPT[@]}" :tunnel:assembleDebug testDebugUnitTest
|
||||
fi
|
||||
|
||||
APK=""
|
||||
|
||||
@@ -14,9 +14,8 @@ source "$ROOT/scripts/android-ndk.sh"
|
||||
export ANDROID_NDK_HOME="$(androidcast_find_ndk_root "$ROOT")"
|
||||
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
|
||||
|
||||
if [[ -f .gitmodules ]]; then
|
||||
git submodule update --init --recursive
|
||||
fi
|
||||
echo "==> Initializing third-party git submodules"
|
||||
bash "$ROOT/scripts/init-third-party-submodules.sh"
|
||||
|
||||
ABIS="${ANDROIDCAST_CI_ABIS:-armeabi-v7a arm64-v8a x86_64}"
|
||||
echo "==> Building libvpx for: $ABIS"
|
||||
@@ -26,7 +25,7 @@ for abi in $ABIS; do
|
||||
./scripts/build-native-codecs.sh "$abi"
|
||||
done
|
||||
|
||||
echo "==> Gradle assembleDebug + unit tests"
|
||||
./gradlew --no-daemon clean assembleDebug testDebugUnitTest
|
||||
echo "==> Gradle :tunnel + assembleDebug + unit tests"
|
||||
./gradlew --no-daemon clean :tunnel:assembleDebug assembleDebug testDebugUnitTest
|
||||
|
||||
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
|
||||
# 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
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
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."
|
||||
exec "$ROOT/scripts/init-third-party-submodules.sh" --only third-party/wireguard-android
|
||||
|
||||
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'
|
||||
project(':session-studio').projectDir = file('desktop/session-studio')
|
||||
|
||||
def wgTunnelDir = file('third-party/wireguard-android/tunnel')
|
||||
if (wgTunnelDir.exists()) {
|
||||
include ':tunnel'
|
||||
project(':tunnel').projectDir = wgTunnelDir
|
||||
def wgTunnelSrc = file('third-party/wireguard-android/tunnel')
|
||||
if (!wgTunnelSrc.isDirectory()) {
|
||||
throw new GradleException(
|
||||
'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
|
||||
|
||||
## 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
|
||||
git submodule update --init third-party/wireguard-android
|
||||
# or:
|
||||
bash scripts/init-third-party-submodules.sh
|
||||
# wireguard only:
|
||||
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/`
|
||||
- Gradle module: `third-party/wireguard-android/tunnel`
|
||||
- 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