Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to v2rayF are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.8] - 2026-06-29

### Fixed

- Android connect crash — pass VPN TUN fd to Xray via `XRAY_TUN_FD` environment variable (required on Android)
- Android connect — set `LD_LIBRARY_PATH` when launching Xray so native dependencies load on Samsung and similar devices
- Android connect — removed double Xray start on connect (VPN first, single core launch)
- Android connect — clearer error when ProcessBuilder cannot exec the core instead of silent force-close

## [1.1.7] - 2026-06-28

### Fixed
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
| Name | GitHub | Role |
|------|--------|------|
| dr mike | [@drmikecrypto](https://github.com/drmikecrypto) | Maintainer |
| hasti1356 | [@hasti1356](https://github.com/hasti1356) | Contributor |

Thank you to everyone who reports issues, suggests features, and helps improve v2rayF.
44 changes: 44 additions & 0 deletions docs/releases/v1.1.8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# v2rayF v1.1.8

Android connect crash fix — TUN fd and process environment.

---

## Highlights

- Fixes force-close on Connect for Samsung Galaxy M21 and similar Android 12 devices
- Xray now receives the VPN file descriptor the way Android Xray-core expects (`XRAY_TUN_FD`)
- Single connect attempt (no probe double-start)

---

## Downloads

| Platform | Package | How to run |
|----------|---------|------------|
| Android ARM64 | `v2rayF-android-arm64.zip` | Install `v2rayF-android-arm64.apk` |

Uninstall v1.1.7 first, then install this APK.

---

## Fixed

- Connect crash from missing `XRAY_TUN_FD` when VPN TUN inbound starts
- Connect crash from missing `LD_LIBRARY_PATH` for `libxray.so` dependencies
- Redundant probe Xray start before VPN on Android
- `Process.IsAlive` compatibility on API 24–25

---

## Safe connect (poor networks)

1. **VPN** — established first with your permission
2. **Xray** — starts once with the TUN fd
3. **Fail** — VPN is torn down immediately; you see an error message instead of a crash

---

## Full changelog

[CHANGELOG.md](https://github.com/drmikecrypto/v2rayF/blob/main/CHANGELOG.md)
82 changes: 67 additions & 15 deletions src/v2rayF.Android/Services/AndroidJavaCoreProcessHost.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Android.App;
using Android.OS;
using Java.IO;
using Java.Lang;
using v2rayF.Services;
using Process = Java.Lang.Process;
using IOException = Java.IO.IOException;

namespace v2rayF.Android.Services;

Expand All @@ -13,6 +17,8 @@
/// </summary>
public sealed class AndroidJavaCoreProcessHost : ICoreProcessHost
{
private const string TunFdEnvKey = "XRAY_TUN_FD";

private readonly object _lock = new();
private Process? _process;
private string _recentOutput = "";
Expand All @@ -22,7 +28,7 @@
get
{
lock (_lock)
return _process is { IsAlive: true };
return IsAlive(_process);
}
}

Expand All @@ -31,35 +37,62 @@
get
{
lock (_lock)
return _process is null || !_process.IsAlive;
return _process is null || !IsAlive(_process);
}
}

public Task StartAsync(
string corePath,
string configPath,
string workingDirectory,
int? tunFd = null,
CancellationToken cancellationToken = default)
{
StopAsync(cancellationToken).GetAwaiter().GetResult();

if (!System.IO.File.Exists(corePath))
throw new System.IO.FileNotFoundException("Xray core not found.", corePath);

if (!System.IO.File.Exists(configPath))
throw new System.IO.FileNotFoundException("Xray config not found.", configPath);

lock (_lock)
_recentOutput = "";

var cmd = new[] { corePath, "run", "-c", configPath };
var builder = new ProcessBuilder(cmd);
builder.Directory(new File(workingDirectory));
builder.RedirectErrorStream(true);
var nativeLibDir = Application.Context?.ApplicationInfo?.NativeLibraryDir ?? workingDirectory;

Process process;
lock (_lock)
try
{
_process = builder.Start();
process = _process;
}
var builder = new ProcessBuilder(corePath, "run", "-c", configPath);
builder.Directory(new Java.IO.File(workingDirectory));
builder.RedirectErrorStream(true);

var env = builder.Environment();
env["LD_LIBRARY_PATH"] = nativeLibDir;

Check warning on line 71 in src/v2rayF.Android/Services/AndroidJavaCoreProcessHost.cs

View workflow job for this annotation

GitHub Actions / build-android

Dereference of a possibly null reference.
env["TMPDIR"] = workingDirectory;

if (process is not null)
_ = Task.Run(() => DrainOutputAsync(process), cancellationToken);
if (tunFd is int fd && fd >= 0)
{
env[TunFdEnvKey] = fd.ToString();
env["xray.tun.fd"] = fd.ToString();
}

Process process;
lock (_lock)
{
_process = builder.Start();
process = _process;

Check warning on line 84 in src/v2rayF.Android/Services/AndroidJavaCoreProcessHost.cs

View workflow job for this annotation

GitHub Actions / build-android

Converting null literal or possible null value to non-nullable type.
}

if (process is not null)
_ = Task.Run(() => DrainOutputAsync(process), cancellationToken);
}
catch (IOException ex)
{
throw new InvalidOperationException(
$"Xray core failed to start: {ex.Message}. Reinstall the app or check that your device is ARM64.",
ex);
}

return Task.CompletedTask;
}
Expand All @@ -73,7 +106,7 @@

try
{
if (_process.IsAlive)
if (IsAlive(_process))
_process.Destroy();
}
catch
Expand All @@ -93,7 +126,7 @@
{
lock (_lock)
{
if (_process is { IsAlive: true })
if (_process is not null && IsAlive(_process))
return _recentOutput.Trim();

if (_process is not null)
Expand All @@ -116,6 +149,25 @@
}
}

private static bool IsAlive(Process? process)
{
if (process is null)
return false;

if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
return process.IsAlive;

try
{
process.ExitValue();
return false;
}
catch (IllegalThreadStateException)
{
return true;
}
}

private void DrainOutputAsync(Process process)
{
try
Expand Down
6 changes: 3 additions & 3 deletions src/v2rayF.Android/v2rayF.Android.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
<SupportedOSPlatformVersion>24</SupportedOSPlatformVersion>
<Nullable>enable</Nullable>
<ApplicationId>com.drmikecrypto.v2rayf</ApplicationId>
<ApplicationVersion>8</ApplicationVersion>
<ApplicationDisplayVersion>1.1.6</ApplicationDisplayVersion>
<ApplicationVersion>9</ApplicationVersion>
<ApplicationDisplayVersion>1.1.8</ApplicationDisplayVersion>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<Version>1.1.6</Version>
<Version>1.1.8</Version>
<Product>v2rayF</Product>
<Description>v2rayF Android proxy client.</Description>
<RepositoryUrl>https://github.com/drmikecrypto/v2rayF</RepositoryUrl>
Expand Down
1 change: 1 addition & 0 deletions src/v2rayF.Core/Services/ICoreProcessHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Task StartAsync(
string corePath,
string configPath,
string workingDirectory,
int? tunFd = null,
CancellationToken cancellationToken = default);

Task StopAsync(CancellationToken cancellationToken = default);
Expand Down
1 change: 1 addition & 0 deletions src/v2rayF.Core/Services/LatencyService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ await _speedtestHost.StartAsync(
corePath,
configPath,
_environment.GetCoresDirectory(),
tunFd: null,
cancellationToken).ConfigureAwait(false);

await WaitForCoreReadyAsync(cancellationToken).ConfigureAwait(false);
Expand Down
1 change: 1 addition & 0 deletions src/v2rayF.Core/Services/ManagedCoreProcessHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public Task StartAsync(
string corePath,
string configPath,
string workingDirectory,
int? tunFd = null,
CancellationToken cancellationToken = default)
{
StopAsync(cancellationToken).GetAwaiter().GetResult();
Expand Down
1 change: 1 addition & 0 deletions src/v2rayF.Core/Services/ProxyCoreService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ await ProcessHost.StartAsync(
ResolveCorePath(),
_configPath,
ResolveCoresDirectory(),
tunFd,
cancellationToken).ConfigureAwait(false);

await WaitForCoreReadyAsync(cancellationToken).ConfigureAwait(false);
Expand Down
34 changes: 14 additions & 20 deletions src/v2rayF/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -455,20 +455,6 @@ private async Task ConnectAndroidAsync(
AppSettings settings,
Action<bool> markVpnEngaged)
{
RunOnUiThread(() => StatusText = $"Checking {server.Name}…");

var probeSettings = new AppSettings
{
RoutingMode = settings.RoutingMode,
CustomDirectRules = settings.CustomDirectRules,
EnableTunMode = false,
EnableSystemProxy = false,
SubscriptionUrl = settings.SubscriptionUrl
};

await _proxyCore.StartAsync(server, probeSettings, null).ConfigureAwait(false);
await _proxyCore.StopAsync().ConfigureAwait(false);

RunOnUiThread(() => StatusText = "Starting VPN…");
var tunFd = await AppServices.Platform.EstablishVpnAsync().ConfigureAwait(false);
if (tunFd is null)
Expand All @@ -480,14 +466,22 @@ private async Task ConnectAndroidAsync(
markVpnEngaged(true);

RunOnUiThread(() => StatusText = $"Starting proxy for {server.Name}…");
await _proxyCore.StartAsync(server, settings, tunFd).ConfigureAwait(false);
await AppServices.Platform.EnableProxyAsync().ConfigureAwait(false);
try
{
await _proxyCore.StartAsync(server, settings, tunFd).ConfigureAwait(false);
await AppServices.Platform.EnableProxyAsync().ConfigureAwait(false);

RunOnUiThread(() =>
RunOnUiThread(() =>
{
StatusText = $"Connected — {server.Name} (VPN)";
IsConnected = true;
});
}
catch
{
StatusText = $"Connected — {server.Name} (VPN)";
IsConnected = true;
});
await SafeTeardownAsync(vpnEngaged: true).ConfigureAwait(false);
throw;
}
}

private async Task SafeTeardownAsync(bool vpnEngaged)
Expand Down
Loading