Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
1f6f538
Enable metered connection setting for VPN builder
dfliaoyue Apr 6, 2026
c339760
Change build process to assembleDebug APK
dfliaoyue Apr 6, 2026
7547b01
Disable signingConfig for debug build
dfliaoyue Apr 6, 2026
57dccbf
Change APK copy path from release to debug
dfliaoyue Apr 6, 2026
b957839
Enable signingConfig for debug build
dfliaoyue Apr 6, 2026
e2f8484
Change build process to assemble release APK
dfliaoyue Apr 6, 2026
1fee9cd
Merge pull request #1 from lhear/main
dfliaoyue Apr 12, 2026
6cb3194
Add fd_helper.c to manage file descriptor flags
dfliaoyue Apr 12, 2026
dd7a6d4
Delete app/src/main/jni/fd_helper.c
dfliaoyue Apr 12, 2026
0f2ff6c
Initial plan
Copilot Apr 13, 2026
f0bd938
Add Xray TUN support: preferences, config injection, service logic, U…
Copilot Apr 13, 2026
211f2c9
Fix compile errors and correct Xray TUN fd mechanism (use XRAY_TUN_FD…
Copilot Apr 13, 2026
9a04c5a
fix: inherit VPN fd into Xray TUN process via native fork+dup2+exec
Copilot Apr 13, 2026
3603027
refactor: address code review comments (malloc array, no !! force-unw…
Copilot Apr 13, 2026
f560277
Merge pull request #2 from dfliaoyue/copilot/add-xray-tun-support
dfliaoyue Apr 13, 2026
f571d8a
更新 ConfigUtils.kt
dfliaoyue Apr 14, 2026
68f43a4
更新 Preferences.kt
dfliaoyue Apr 14, 2026
af4d3ce
更新 ConfigUtils.kt
dfliaoyue Apr 14, 2026
f64257a
更新 Preferences.kt(恢复到原项目设置)
dfliaoyue Apr 14, 2026
db95d89
更新 Preferences.kt
dfliaoyue Apr 14, 2026
3a46b84
更新 TProxyService.kt
dfliaoyue Apr 14, 2026
b81273f
Fix connectivity test for HEV TUN mode (credential auth) and Xray TUN…
Copilot Apr 14, 2026
dcd03d1
Add EOF checks in SOCKS5 helper to prevent silent -1 parsing
Copilot Apr 14, 2026
e3b6a18
Revert HEV TUN connectivity to original; keep only Xray TUN fix
Copilot Apr 14, 2026
6bc2a43
Fix connectivity test: use SOCKS proxy in Xray TUN mode too
Copilot Apr 14, 2026
dc8f468
Fix Xray TUN connectivity test: inject ephemeral SOCKS inbound at run…
Copilot Apr 14, 2026
511e928
Fix slash escaping in injectEphemeralSocks for consistency
Copilot Apr 14, 2026
de44b8c
Revert ephemeral SOCKS; test via VPN-network-bound socket in Xray TUN…
Copilot Apr 14, 2026
14b8780
Fix SSL socket timeout and resource cleanup in Xray TUN connectivity …
Copilot Apr 14, 2026
67b0c64
Fix crash in connectivity test: add ACCESS_NETWORK_STATE permission a…
Copilot Apr 14, 2026
41f9636
Fix connectivity test in Xray TUN mode: use Network.openConnection() …
Copilot Apr 14, 2026
b9e57fe
Fix HEV TUN connectivity test: use createUnresolved to send hostname …
Copilot Apr 14, 2026
15bbcfc
Fix Xray TUN connectivity test: remove special branch, use SOCKS5 for…
Copilot Apr 14, 2026
466d737
Fix Xray TUN connectivity test: bind to VPN network via ConnectivityM…
Copilot Apr 14, 2026
0d95f19
Fix Xray TUN connectivity test: use direct internet test (excluded UI…
Copilot Apr 14, 2026
9ac035a
feat: Xray TUN connectivity test via runtime SOCKS inbound (HandlerSe…
Copilot Apr 14, 2026
1b75f67
fix: compilation error, 127.0.0.1 listen, high port, 3s SOCKS timeout
Copilot Apr 14, 2026
6a74f46
refine: SecureRandom for port, named constants, localhost comment
Copilot Apr 14, 2026
dce7f40
fix: xray TUN connectivity - NO_AUTH SOCKS5, retry logic, error logging
Copilot Apr 14, 2026
c611bf1
refine: fix review issues - delay(), socket error capture, comment wo…
Copilot Apr 14, 2026
fde5b9c
fix: use GUI SOCKS5 credentials for xray TUN connectivity test inbound
Copilot Apr 14, 2026
38dbefa
Fix xray TUN connectivity: use TypedMessage with bare type names inst…
Copilot Apr 14, 2026
52b758e
Fix latency timing: measure only network round-trip, exclude xray TUN…
Copilot Apr 14, 2026
853b985
fix: measure latency after tunnel+SSL established, not before connect()
Copilot Apr 14, 2026
1fe5095
fix: xray TUN uses existing SOCKS proxy instead of cold-start temp in…
Copilot Apr 14, 2026
edb075f
fix: restore xray TUN temp inbound with warmup+timed dual-request, SO…
Copilot Apr 14, 2026
c7647bd
revert: SOCKS_LIFETIME_MS back to 5s, remove warmup request
Copilot Apr 14, 2026
35ac55e
Merge pull request #3 from dfliaoyue/copilot/check-fix-hev-xray-tun-c…
dfliaoyue Apr 14, 2026
2b256ca
Merge branch 'main' into main
dfliaoyue Apr 14, 2026
21aa521
feat: route TUN-mode app traffic through ephemeral SOCKS5 inbound wit…
Copilot Apr 15, 2026
ef662e3
fix: restore raw Socket connectivity test to match upstream (#5)
Copilot Apr 15, 2026
ec1efd9
style: align comment density with lhear/simpleXray upstream (#6)
Copilot Apr 15, 2026
893bed2
Add test purpose note to README
dfliaoyue Apr 15, 2026
9541344
Update README to clarify project origin and purpose
dfliaoyue Apr 15, 2026
60588f1
还原 README.md
dfliaoyue Apr 15, 2026
58a5492
fix(xray_exec): eliminate malloc/opendir in child after fork to preve…
Copilot Apr 16, 2026
7fc9110
Clean up inline comments in xray_exec.c, keep only necessary ones (#8)
Copilot Apr 16, 2026
37639c7
perf: buffer log file writes instead of per-line flush (#10)
Copilot Apr 17, 2026
790cc5b
Revert "perf: buffer log file writes instead of per-line flush (#10)"…
dfliaoyue Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/src/main/jni/xray-exec/Android.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := xray-exec
LOCAL_SRC_FILES := xray_exec.c
LOCAL_LDLIBS := -llog

include $(BUILD_SHARED_LIBRARY)
160 changes: 160 additions & 0 deletions app/src/main/jni/xray-exec/xray_exec.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* xray_exec.c – spawn the Xray binary with the Android VPN fd properly inherited.
*
* Problem: Android's ProcessBuilder (fork + exec) honours FD_CLOEXEC. The fd
* returned by VpnService.Builder.establish() always has FD_CLOEXEC set, so it
* is automatically closed before the child process starts. Passing its number
* via the XRAY_TUN_FD environment variable therefore gives Xray an invalid fd.
*
* Fix: fork() here in native code, then dup2() the VPN fd to a fixed target fd
* (CHILD_TUN_FD = 4) before exec(). dup2() does not copy FD_CLOEXEC, so fd 4
* survives exec and is visible to Xray as its TUN fd.
*
* The merged Xray config JSON is delivered to Xray via stdin so that no
* sensitive data ever touches the file system. A stdout pipe lets the caller
* read Xray logs.
*
* JNI signature:
* int[] TProxyService.nativeSpawnXray(String xrayPath, String assetDir, int vpnFd)
* Returns int[3] = { pid, stdout_read_fd, stdin_write_fd }, or null on failure.
*/

#include <jni.h>

#include <android/log.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <unistd.h>

#define LOG_TAG "XrayExec"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

/* fd number reserved for the VPN fd inside the Xray process */
#define CHILD_TUN_FD 4

/*
* Close every open fd > 2 except keep_fd.
* Uses only async-signal-safe syscalls so it is safe to call after fork().
*/
static void close_extra_fds(int keep_fd)
{
long max_fd = sysconf(_SC_OPEN_MAX);
if (max_fd <= 0 || max_fd > 65536) max_fd = 1024;
for (int fd = 3; fd < (int)max_fd; fd++) {
if (fd != keep_fd) close(fd);
}
}

JNIEXPORT jintArray JNICALL
Java_com_simplexray_an_service_TProxyService_nativeSpawnXray(
JNIEnv *env, jclass clazz,
jstring xray_path_j,
jstring asset_dir_j,
jint vpn_fd)
{
const char *xray_path = (*env)->GetStringUTFChars(env, xray_path_j, NULL);
const char *asset_dir = (*env)->GetStringUTFChars(env, asset_dir_j, NULL);

int stdin_pipe[2] = {-1, -1};
int stdout_pipe[2] = {-1, -1};

if (pipe(stdin_pipe) < 0 || pipe(stdout_pipe) < 0) {
LOGE("pipe() failed: %s", strerror(errno));
close(stdin_pipe[0]); close(stdin_pipe[1]);
close(stdout_pipe[0]); close(stdout_pipe[1]);
(*env)->ReleaseStringUTFChars(env, xray_path_j, xray_path);
(*env)->ReleaseStringUTFChars(env, asset_dir_j, asset_dir);
return NULL;
}

char asset_env[4096];
char tun_fd_env[64];
snprintf(asset_env, sizeof(asset_env), "XRAY_LOCATION_ASSET=%s", asset_dir);
snprintf(tun_fd_env, sizeof(tun_fd_env), "XRAY_TUN_FD=%d", CHILD_TUN_FD);

extern char **environ;
int parent_ec = 0;
while (environ[parent_ec]) parent_ec++;

char **new_env = (char **)malloc((size_t)(parent_ec + 3) * sizeof(char *));
if (!new_env) {
LOGE("malloc failed for new_env");
close(stdin_pipe[0]); close(stdin_pipe[1]);
close(stdout_pipe[0]); close(stdout_pipe[1]);
(*env)->ReleaseStringUTFChars(env, xray_path_j, xray_path);
(*env)->ReleaseStringUTFChars(env, asset_dir_j, asset_dir);
return NULL;
}
int ni = 0;
for (int i = 0; i < parent_ec; i++) {
if (strncmp(environ[i], "XRAY_LOCATION_ASSET=", 20) == 0) continue;
if (strncmp(environ[i], "XRAY_TUN_FD=", 12) == 0) continue;
new_env[ni++] = environ[i];
}
new_env[ni++] = asset_env;
new_env[ni++] = tun_fd_env;
new_env[ni] = NULL;

char *argv[] = { (char *)xray_path, NULL };

pid_t pid = fork();
if (pid < 0) {
LOGE("fork() failed: %s", strerror(errno));
free(new_env);
close(stdin_pipe[0]); close(stdin_pipe[1]);
close(stdout_pipe[0]); close(stdout_pipe[1]);
(*env)->ReleaseStringUTFChars(env, xray_path_j, xray_path);
(*env)->ReleaseStringUTFChars(env, asset_dir_j, asset_dir);
return NULL;
}

if (pid == 0) {
/* child */
prctl(PR_SET_PDEATHSIG, SIGKILL); /* die with parent */

dup2(stdin_pipe[0], STDIN_FILENO);
dup2(stdout_pipe[1], STDOUT_FILENO);
dup2(stdout_pipe[1], STDERR_FILENO);

close(stdin_pipe[0]); close(stdin_pipe[1]);
close(stdout_pipe[0]); close(stdout_pipe[1]);

/* dup2 does not copy FD_CLOEXEC, so CHILD_TUN_FD survives exec */
if ((int)vpn_fd >= 0 && (int)vpn_fd != CHILD_TUN_FD) {
if (dup2((int)vpn_fd, CHILD_TUN_FD) < 0) {
_exit(1);
}
}

close_extra_fds(CHILD_TUN_FD);

execve(xray_path, argv, new_env);
_exit(1);
}

/* parent */
free(new_env);

close(stdin_pipe[0]);
close(stdout_pipe[1]);

(*env)->ReleaseStringUTFChars(env, xray_path_j, xray_path);
(*env)->ReleaseStringUTFChars(env, asset_dir_j, asset_dir);

LOGI("Spawned xray pid=%d stdout_read_fd=%d stdin_write_fd=%d",
pid, stdout_pipe[0], stdin_pipe[1]);

jintArray result = (*env)->NewIntArray(env, 3);
if (!result) {
close(stdout_pipe[0]); close(stdin_pipe[1]);
return NULL;
}
jint arr[3] = { (jint)pid, (jint)stdout_pipe[0], (jint)stdin_pipe[1] };
(*env)->SetIntArrayRegion(env, result, 0, 3, arr);
return result;
}
79 changes: 75 additions & 4 deletions app/src/main/kotlin/com/simplexray/an/common/ConfigUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@ import org.json.JSONObject
object ConfigUtils {
private const val TAG = "ConfigUtils"

fun extractTunMtu(configContent: String): Int? {
try {
val jsonObject = JSONObject(configContent)
val inbounds = jsonObject.optJSONArray("inbounds") ?: return null
for (i in 0 until inbounds.length()) {
val inbound = inbounds.optJSONObject(i) ?: continue
if (inbound.optString("protocol") == "tun") {
return inbound.optJSONObject("settings")?.optInt("MTU", -1)
?.takeIf { it > 0 }
}
}
} catch (e: JSONException) {
Log.e(TAG, "Error parsing JSON for TUN MTU extraction", e)
}
return null
}

@Throws(JSONException::class)
fun formatConfigContent(content: String): String {
val jsonObject = JSONObject(content)
Expand Down Expand Up @@ -37,18 +54,37 @@ object ConfigUtils {
servicesArray.put("StatsService")
apiObject.put("services", servicesArray)

jsonObject.put("api", apiObject)
jsonObject.put("stats", JSONObject())

val policyObject = JSONObject()
val systemObject = JSONObject()
systemObject.put("statsOutboundUplink", true)
systemObject.put("statsOutboundDownlink", true)
policyObject.put("system", systemObject)

jsonObject.put("api", apiObject)
jsonObject.put("stats", JSONObject())
jsonObject.put("policy", policyObject)

return jsonObject.toString(2)
var result = jsonObject.toString(2)
result = result.replace("\\/", "/")
return result
}

@Throws(JSONException::class)
fun mergeAdditionalInbounds(baseConfig: String, extraInboundsJson: String): String {
val base = JSONObject(baseConfig)
val extra = JSONObject(extraInboundsJson)

val baseInbounds = base.optJSONArray("inbounds") ?: org.json.JSONArray()
val extraInbounds = extra.optJSONArray("inbounds") ?: return baseConfig

for (i in 0 until extraInbounds.length()) {
baseInbounds.put(extraInbounds.get(i))
}
base.put("inbounds", baseInbounds)

var result = base.toString(2)
result = result.replace("\\/", "/")
return result
}

fun extractPortsFromJson(jsonContent: String): Set<Int> {
Expand Down Expand Up @@ -87,5 +123,40 @@ object ConfigUtils {
}
}
}

fun buildTempSocksConfigJson(
listenAddress: String,
port: Int,
tag: String,
username: String,
password: String,
): String {
require(port in 1..65535) { "port must be in 1..65535, got $port" }

val account = JSONObject()
account.put("user", username)
account.put("pass", password)
val accountsArray = org.json.JSONArray()
accountsArray.put(account)

val settings = JSONObject()
settings.put("auth", "password")
settings.put("udp", false)
settings.put("accounts", accountsArray)

val inbound = JSONObject()
inbound.put("tag", tag)
inbound.put("port", port)
inbound.put("listen", listenAddress)
inbound.put("protocol", "socks")
inbound.put("settings", settings)

val inboundsArray = org.json.JSONArray()
inboundsArray.put(inbound)

val root = JSONObject()
root.put("inbounds", inboundsArray)
return root.toString(2)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ class VlessLinkConverter: ConfigFormatConverter {

val socksPort = Preferences(context).socksPort

// Build config JSON
val config = JSONObject(
mapOf(
"log" to mapOf("loglevel" to "warning"),
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/kotlin/com/simplexray/an/prefs/Preferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,21 @@ class Preferences(context: Context) {
setValueInProvider(DISABLE_VPN, value)
}

var useXrayTun: Boolean
get() = getBooleanPref(USE_XRAY_TUN, false)
set(value) {
setValueInProvider(USE_XRAY_TUN, value)
}

val isXrayTunActive: Boolean
get() = useXrayTun && !disableVpn

val tunnelMtu: Int
get() = 8500

val tunnelMtuForXrayTun: Int
get() = 1500

val tunnelIpv4Address: String
get() = "198.18.0.1"

Expand Down Expand Up @@ -336,6 +348,7 @@ class Preferences(context: Context) {
const val CUSTOM_GEOSITE_IMPORTED: String = "CustomGeositeImported"
const val CONFIG_FILES_ORDER: String = "ConfigFilesOrder"
const val DISABLE_VPN: String = "DisableVpn"
const val USE_XRAY_TUN: String = "UseXrayTun"
const val CONNECTIVITY_TEST_TARGET: String = "ConnectivityTestTarget"
const val CONNECTIVITY_TEST_TIMEOUT: String = "ConnectivityTestTimeout"
const val GEOIP_URL: String = "GeoipUrl"
Expand Down
Loading
Loading