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
Binary file modified Assets/engine/xray.exe
Binary file not shown.
3 changes: 3 additions & 0 deletions Models/AppSettings.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Text.Json.Nodes;
using XrayUI.Services;

namespace XrayUI.Models
{
Expand All @@ -11,6 +12,8 @@ public class AppSettings
/// <summary>Whether TUN mode is enabled.</summary>
public bool IsTunMode { get; set; } = false;
public string? LastTunServerHost { get; set; }
public int TunMtu { get; set; } = XrayConfigConstants.TunMtuDefault;
public string TunOutboundInterface { get; set; } = XrayConfigConstants.TunOutboundInterfaceAuto;
public bool IsStartupEnabled { get; set; } = false;
public bool IsAutoConnect { get; set; } = false;
/// <summary>true = global proxy (default); false = do not take over the system proxy.</summary>
Expand Down
21 changes: 20 additions & 1 deletion Services/DialogService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading;
using System.Threading.Tasks;
using System.ComponentModel;
Expand Down Expand Up @@ -469,6 +469,25 @@ public async Task<bool> ShowConfirmationAsync(string title, string message, stri
return result == ContentDialogResult.Primary;
}

public async Task<bool> ShowTunConfirmationDialogAsync(AppSettings settings)
{
var content = new TunConfirmationDialog(settings.TunMtu, settings.TunOutboundInterface);

var dialog = CreateDialog();
dialog.Title = "开启TUN模式";
dialog.Content = content;
dialog.PrimaryButtonText = "确认";
dialog.CloseButtonText = "取消";
dialog.DefaultButton = ContentDialogButton.Primary;

if (await dialog.ShowAsync() != ContentDialogResult.Primary)
return false;

settings.TunMtu = content.Mtu;
settings.TunOutboundInterface = content.SelectedInterface;
return true;
}

public async Task ShowErrorAsync(string title, string message, XamlRoot? xamlRoot = null)
{
var dialog = CreateDialog(xamlRoot);
Expand Down
6 changes: 6 additions & 0 deletions Services/IDialogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ public interface IDialogService
Task<int?> ShowEditPortDialogAsync(int currentPort);
Task ShowErrorAsync(string title, string message, XamlRoot? xamlRoot = null);
Task<bool> ShowConfirmationAsync(string title, string message, string confirmText = "确定", string cancelText = "取消", bool isDanger = false);
/// <summary>
/// Shows the TUN confirmation dialog. Mutates <paramref name="settings"/>.TunMtu and
/// <paramref name="settings"/>.TunOutboundInterface in-place on confirm. Returns true if
/// the user confirmed (caller must persist), false if cancelled.
/// </summary>
Task<bool> ShowTunConfirmationDialogAsync(AppSettings settings);
Task ShowShareLinkDialogAsync(string serverName, string link);
Task<(bool enabled, bool autoConnect)?> ShowStartupDialogAsync(bool currentEnabled, bool currentAutoConnect);

Expand Down
114 changes: 28 additions & 86 deletions Services/TunService.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using XrayUI.Helpers;

Expand All @@ -21,8 +19,7 @@ public class TunService
{
private readonly string _engineDirectory;

/// <summary>Default TUN interface name; must match the name field in XrayConfigBuilder.BuildTunInbound.</summary>
private const string DefaultTunInterfaceName = "xray-tun";
private const string TunInterfaceName = XrayConfigConstants.TunInterfaceName;

public TunService()
{
Expand All @@ -42,54 +39,39 @@ public bool IsWintunAvailable()
public string GetExpectedWintunPath() => Path.Combine(_engineDirectory, "wintun.dll");

/// <summary>
/// Finds the physical interface Windows would use for normal outbound IPv4 traffic.
/// TUN mode binds xray outbounds to this interface to avoid sending xray's own
/// proxy connection back into the TUN adapter.
/// Best-effort reset of stale DNS server entries that Windows can persist on the
/// xray-tun adapter between runs. Uses netsh (fast, ~10ms) so it doesn't stall
/// startup or shutdown. Safe to call when the adapter doesn't exist — netsh emits
/// a non-zero exit code but doesn't abort the batch (commands are chained with `&`).
/// Skipped silently when not elevated: a DNS reset isn't worth a UAC prompt, and
/// the cleanup-path callers already pay UAC via <see cref="CleanupTunRoutes"/>.
/// </summary>
public string? DetectDefaultOutboundInterfaceName()
public void ResetTunDnsServers()
{
if (!AdminHelper.IsAdministrator())
return;

try
{
var localAddress = GetDefaultOutboundAddress();
if (localAddress is null)
{
Debug.WriteLine("[TunService] Could not determine the default outbound IPv4 address.");
return null;
}

var match = NetworkInterface.GetAllNetworkInterfaces()
.Where(IsCandidateOutboundInterface)
.Select(nic => new
{
Interface = nic,
Properties = nic.GetIPProperties()
})
.Where(item => item.Properties.UnicastAddresses.Any(address =>
address.Address.AddressFamily == AddressFamily.InterNetwork
&& address.Address.Equals(localAddress)))
.Select(item => item.Interface)
.FirstOrDefault();

if (match is null)
{
Debug.WriteLine($"[TunService] Could not map outbound address {localAddress} to a usable interface.");
return null;
}

Debug.WriteLine($"[TunService] Default outbound interface: {match.Name} ({localAddress})");
return match.Name;
RunElevatedBatch(BuildDnsResetBatch());
}
catch (Exception ex)
{
Debug.WriteLine($"[TunService] Default outbound interface detection failed: {ex.Message}");
return null;
Debug.WriteLine($"[TunService] TUN DNS 重置失败: {ex.Message}");
}
}

private static List<string> BuildDnsResetBatch() =>
[
$"netsh interface ipv4 set dnsservers \"{TunInterfaceName}\" source=dhcp",
$"netsh interface ipv6 set dnsservers \"{TunInterfaceName}\" source=dhcp",
];

/// <summary>
/// Fallback cleanup: xray removes its own routes on normal exit, so this is only used
/// after an abnormal xray exit or when routes remain after exit. Removes the 0.0.0.0/0
/// fallback route plus the direct route to the server.
/// fallback route plus the direct route to the server, and resets any stale DNS entries
/// on the xray-tun adapter — all in one elevated batch so the user sees at most one UAC.
/// </summary>
public void CleanupTunRoutes(string? serverAddress)
{
Expand All @@ -103,9 +85,9 @@ public void CleanupTunRoutes(string? serverAddress)
{
// 0.0.0.0/0 is what current xray adds; the /1 split-routes are residue
// from earlier routing schemes that may still be lying around.
$"netsh interface ipv4 delete route 0.0.0.0/0 \"{DefaultTunInterfaceName}\" store=active",
$"netsh interface ipv4 delete route 0.0.0.0/1 \"{DefaultTunInterfaceName}\" store=active",
$"netsh interface ipv4 delete route 128.0.0.0/1 \"{DefaultTunInterfaceName}\" store=active",
$"netsh interface ipv4 delete route 0.0.0.0/0 \"{TunInterfaceName}\" store=active",
$"netsh interface ipv4 delete route 0.0.0.0/1 \"{TunInterfaceName}\" store=active",
$"netsh interface ipv4 delete route 128.0.0.0/1 \"{TunInterfaceName}\" store=active",
// Legacy route.exe form for the same /1 split-routes.
"route delete 0.0.0.0 mask 128.0.0.0",
"route delete 128.0.0.0 mask 128.0.0.0",
Expand All @@ -115,13 +97,15 @@ public void CleanupTunRoutes(string? serverAddress)
// does not resolve domains. Skip server-IP cleanup unless it is IPv4.
if (TryParseSafeIPv4Address(serverAddress, out var serverIPv4))
{
batch.Add($"netsh interface ipv4 delete route {serverIPv4}/32 \"{DefaultTunInterfaceName}\" store=active");
batch.Add($"netsh interface ipv4 delete route {serverIPv4}/32 \"{TunInterfaceName}\" store=active");
batch.Add($"route delete {serverIPv4} mask 255.255.255.255");
}

foreach (var dns in legacyDnsServers)
batch.Add($"route delete {dns} mask 255.255.255.255");

batch.AddRange(BuildDnsResetBatch());

RunElevatedBatch(batch);
Debug.WriteLine("[TunService] TUN 路由兜底清理完成");
}
Expand All @@ -147,48 +131,6 @@ private static bool TryParseSafeIPv4Address(string? value, out string address)
return true;
}

private static IPAddress? GetDefaultOutboundAddress()
{
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.Connect("8.8.8.8", 53);
return (socket.LocalEndPoint as IPEndPoint)?.Address;
}

private bool IsCandidateOutboundInterface(NetworkInterface nic)
{
if (nic.OperationalStatus != OperationalStatus.Up)
return false;

if (nic.NetworkInterfaceType is NetworkInterfaceType.Loopback or NetworkInterfaceType.Tunnel)
return false;

var name = nic.Name ?? string.Empty;
var description = nic.Description ?? string.Empty;
var combined = $"{name} {description}";

return !ContainsAny(combined,
DefaultTunInterfaceName,
"wintun",
"xray",
"loopback",
"pseudo-interface",
"virtualbox",
"vmware",
"hyper-v virtual",
"vethernet");
}

private static bool ContainsAny(string value, params string[] needles)
{
foreach (var needle in needles)
{
if (value.Contains(needle, StringComparison.OrdinalIgnoreCase))
return true;
}

return false;
}

/// <summary>
/// Runs a batch of full command lines (e.g. "netsh interface ipv4 ...", "route delete ...")
/// in a single cmd.exe — chained with `&amp;` so a failure in one doesn't abort the rest.
Expand Down
32 changes: 19 additions & 13 deletions Services/XrayConfigBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text.Json;
Expand Down Expand Up @@ -27,15 +27,14 @@ public static class XrayConfigBuilder
public static string Build(
ServerEntry server,
AppSettings settings,
string? tunOutboundInterfaceName = null,
IEnumerable<ServerEntry>? availableServers = null)
{
var config = new JsonObject
{
["log"] = BuildLog(settings),
["dns"] = BuildDns(settings),
["inbounds"] = BuildInbounds(settings),
["outbounds"] = BuildOutbounds(server, settings, tunOutboundInterfaceName, availableServers),
["outbounds"] = BuildOutbounds(server, settings, availableServers),
["routing"] = BuildRouting(settings)
};

Expand Down Expand Up @@ -124,11 +123,11 @@ private static JsonObject BuildTunInbound(AppSettings settings)
["protocol"] = "tun",
["settings"] = new JsonObject
{
["name"] = "xray-tun",
["MTU"] = 9000,
["name"] = XrayConfigConstants.TunInterfaceName,
["MTU"] = XrayConfigConstants.NormalizeTunMtu(settings.TunMtu),
["gateway"] = CreateStringArray("172.18.0.1/30"),
["autoSystemRoutingTable"] = CreateStringArray("0.0.0.0/0"),
["autoOutboundsInterface"] = "auto"
["autoOutboundsInterface"] = XrayConfigConstants.TunOutboundInterfaceAuto
},
["sniffing"] = sniffing,
};
Expand All @@ -137,7 +136,6 @@ private static JsonObject BuildTunInbound(AppSettings settings)
private static JsonArray BuildOutbounds(
ServerEntry server,
AppSettings settings,
string? tunOutboundInterfaceName,
IEnumerable<ServerEntry>? availableServers)
{
var list = new JsonArray();
Expand Down Expand Up @@ -194,23 +192,31 @@ private static JsonArray BuildOutbounds(
});
}

if (settings.IsTunMode && !string.IsNullOrWhiteSpace(tunOutboundInterfaceName))
var outboundInterface = NormalizeTunOutboundInterface(settings.TunOutboundInterface);
if (settings.IsTunMode && outboundInterface is not null)
{
// sockopt.interface only matters for outbounds that actually open sockets
// to remote hosts. block (blackhole) drops traffic without a socket, and
// dns-out is xray-internal — applying the binding to them produces
// redundant fields in the generated config.
foreach (var outbound in list.OfType<JsonObject>())
{
var tag = outbound["tag"]?.GetValue<string>();
if (tag is ProxyOutboundTag or DirectOutboundTag or ChainEntryOutboundTag)
ApplyOutboundInterface(outbound, tunOutboundInterfaceName);
ApplyOutboundInterface(outbound, outboundInterface);
}
}

return list;
}

private static string? NormalizeTunOutboundInterface(string? interfaceName)
{
if (string.IsNullOrWhiteSpace(interfaceName))
return null;

var value = interfaceName.Trim();
return string.Equals(value, XrayConfigConstants.TunOutboundInterfaceAuto, StringComparison.OrdinalIgnoreCase)
? null
: value;
}

private static (ServerEntry entryServer, ServerEntry exitServer) ResolveChainServers(
ServerEntry chain,
IEnumerable<ServerEntry>? availableServers)
Expand Down
11 changes: 11 additions & 0 deletions Services/XrayConfigConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,16 @@ internal static class XrayConfigConstants
// FakeDNS IP pools. 198.18.0.0/15 is RFC-2544 benchmarking space (safe to reuse).
public const string FakeDnsPoolV4 = "198.18.0.0/15";
public const string FakeDnsPoolV6 = "fc00::/18";

// TUN adapter: the inbound name in the xray config must match the Windows
// interface alias used by TunService for adapter operations (DNS reset, route delete).
public const string TunInterfaceName = "xray-tun";
public const string TunOutboundInterfaceAuto = "auto";
public const int TunMtuMin = 68;
public const int TunMtuMax = 9000;
public const int TunMtuDefault = 1500;

public static int NormalizeTunMtu(int mtu) =>
mtu >= TunMtuMin && mtu <= TunMtuMax ? mtu : TunMtuDefault;
}
}
Loading
Loading