diff --git a/Assets/engine/xray.exe b/Assets/engine/xray.exe
index d2f3b96..a42cf6b 100644
Binary files a/Assets/engine/xray.exe and b/Assets/engine/xray.exe differ
diff --git a/Models/AppSettings.cs b/Models/AppSettings.cs
index 9b76108..b90a78b 100644
--- a/Models/AppSettings.cs
+++ b/Models/AppSettings.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Text.Json.Nodes;
+using XrayUI.Services;
namespace XrayUI.Models
{
@@ -11,6 +12,8 @@ public class AppSettings
/// Whether TUN mode is enabled.
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;
/// true = global proxy (default); false = do not take over the system proxy.
diff --git a/Services/DialogService.cs b/Services/DialogService.cs
index f926462..6b99f8a 100644
--- a/Services/DialogService.cs
+++ b/Services/DialogService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Threading;
using System.Threading.Tasks;
using System.ComponentModel;
@@ -469,6 +469,25 @@ public async Task ShowConfirmationAsync(string title, string message, stri
return result == ContentDialogResult.Primary;
}
+ public async Task 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);
diff --git a/Services/IDialogService.cs b/Services/IDialogService.cs
index 473c813..b646180 100644
--- a/Services/IDialogService.cs
+++ b/Services/IDialogService.cs
@@ -15,6 +15,12 @@ public interface IDialogService
Task ShowEditPortDialogAsync(int currentPort);
Task ShowErrorAsync(string title, string message, XamlRoot? xamlRoot = null);
Task ShowConfirmationAsync(string title, string message, string confirmText = "确定", string cancelText = "取消", bool isDanger = false);
+ ///
+ /// Shows the TUN confirmation dialog. Mutates .TunMtu and
+ /// .TunOutboundInterface in-place on confirm. Returns true if
+ /// the user confirmed (caller must persist), false if cancelled.
+ ///
+ Task ShowTunConfirmationDialogAsync(AppSettings settings);
Task ShowShareLinkDialogAsync(string serverName, string link);
Task<(bool enabled, bool autoConnect)?> ShowStartupDialogAsync(bool currentEnabled, bool currentAutoConnect);
diff --git a/Services/TunService.cs b/Services/TunService.cs
index 250b746..48f6290 100644
--- a/Services/TunService.cs
+++ b/Services/TunService.cs
@@ -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;
@@ -21,8 +19,7 @@ public class TunService
{
private readonly string _engineDirectory;
- /// Default TUN interface name; must match the name field in XrayConfigBuilder.BuildTunInbound.
- private const string DefaultTunInterfaceName = "xray-tun";
+ private const string TunInterfaceName = XrayConfigConstants.TunInterfaceName;
public TunService()
{
@@ -42,54 +39,39 @@ public bool IsWintunAvailable()
public string GetExpectedWintunPath() => Path.Combine(_engineDirectory, "wintun.dll");
///
- /// 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 .
///
- 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 BuildDnsResetBatch() =>
+ [
+ $"netsh interface ipv4 set dnsservers \"{TunInterfaceName}\" source=dhcp",
+ $"netsh interface ipv6 set dnsservers \"{TunInterfaceName}\" source=dhcp",
+ ];
+
///
/// 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.
///
public void CleanupTunRoutes(string? serverAddress)
{
@@ -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",
@@ -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 路由兜底清理完成");
}
@@ -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;
- }
-
///
/// Runs a batch of full command lines (e.g. "netsh interface ipv4 ...", "route delete ...")
/// in a single cmd.exe — chained with `&` so a failure in one doesn't abort the rest.
diff --git a/Services/XrayConfigBuilder.cs b/Services/XrayConfigBuilder.cs
index c6d0559..66ccf3f 100644
--- a/Services/XrayConfigBuilder.cs
+++ b/Services/XrayConfigBuilder.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq;
using System.Collections.Generic;
using System.Text.Json;
@@ -27,7 +27,6 @@ public static class XrayConfigBuilder
public static string Build(
ServerEntry server,
AppSettings settings,
- string? tunOutboundInterfaceName = null,
IEnumerable? availableServers = null)
{
var config = new JsonObject
@@ -35,7 +34,7 @@ public static string Build(
["log"] = BuildLog(settings),
["dns"] = BuildDns(settings),
["inbounds"] = BuildInbounds(settings),
- ["outbounds"] = BuildOutbounds(server, settings, tunOutboundInterfaceName, availableServers),
+ ["outbounds"] = BuildOutbounds(server, settings, availableServers),
["routing"] = BuildRouting(settings)
};
@@ -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,
};
@@ -137,7 +136,6 @@ private static JsonObject BuildTunInbound(AppSettings settings)
private static JsonArray BuildOutbounds(
ServerEntry server,
AppSettings settings,
- string? tunOutboundInterfaceName,
IEnumerable? availableServers)
{
var list = new JsonArray();
@@ -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())
{
var tag = outbound["tag"]?.GetValue();
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? availableServers)
diff --git a/Services/XrayConfigConstants.cs b/Services/XrayConfigConstants.cs
index 1dce0e8..b430f1c 100644
--- a/Services/XrayConfigConstants.cs
+++ b/Services/XrayConfigConstants.cs
@@ -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;
}
}
diff --git a/ViewModels/ControlPanelViewModel.cs b/ViewModels/ControlPanelViewModel.cs
index 8dc7d2e..d571661 100644
--- a/ViewModels/ControlPanelViewModel.cs
+++ b/ViewModels/ControlPanelViewModel.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Threading;
@@ -235,15 +235,13 @@ private async Task StartSelectedServerAsync()
if (IsAutoConnect)
appSettings.LastAutoConnectServerId = server.Id;
- string? tunOutboundInterfaceName = null;
if (IsTunMode)
{
- tunOutboundInterfaceName = await RunTunPreflightAsync("TUN mode error");
- if (tunOutboundInterfaceName is null) return false;
+ if (!await RunTunPreflightAsync("TUN mode error")) return false;
await CleanupPersistedTunRoutesAsync(appSettings);
}
- var configJson = XrayConfigBuilder.Build(server, appSettings, tunOutboundInterfaceName, GetAllServers());
+ var configJson = XrayConfigBuilder.Build(server, appSettings, GetAllServers());
var ok = await _xray.StartAsync(configJson);
if (!ok)
@@ -393,29 +391,21 @@ private async Task HandleReapplyFailureAsync(string detail)
///
- /// Runs the shared TUN-mode preflight: wintun availability, outbound interface detection,
- /// and system-proxy clearing. Returns the detected interface name, or null when a check
- /// fails (the user has already seen an error dialog with ).
+ /// Runs the shared TUN-mode preflight: wintun availability and system-proxy clearing.
+ /// Xray-core handles outbound interface selection through autoOutboundsInterface="auto".
///
- private async Task RunTunPreflightAsync(string errorTitle)
+ private async Task RunTunPreflightAsync(string errorTitle)
{
if (!_tunService.IsWintunAvailable())
{
await _dialogs.ShowErrorAsync(errorTitle,
$"Could not find wintun.dll\nPath: {_tunService.GetExpectedWintunPath()}");
- return null;
- }
-
- var iface = _tunService.DetectDefaultOutboundInterfaceName();
- if (string.IsNullOrWhiteSpace(iface))
- {
- await _dialogs.ShowErrorAsync(errorTitle,
- "Could not determine the default outbound network interface. Please check that Wi-Fi/Ethernet is connected, then try again.");
- return null;
+ return false;
}
+ _tunService.ResetTunDnsServers();
SystemProxyService.ClearProxy();
- return iface;
+ return true;
}
private void CleanupTunRoutesSafely()
@@ -571,13 +561,10 @@ private async Task HandleTunToggleAsync(bool wantEnable)
IsTunMode = false;
_isTunInternalUpdate = false;
- var confirmed = await _dialogs.ShowConfirmationAsync(
- "开启TUN模式",
- "开启 TUN 模式需要管理员权限,程序将会重启,是否继续?",
- "确认",
- "取消");
+ var appSettings = await _settings.LoadSettingsAsync();
+ if (!await _dialogs.ShowTunConfirmationDialogAsync(appSettings)) return;
- if (!confirmed) return;
+ await TrySaveSettingsAsync(appSettings, "TUN mode settings save");
RestartAsAdmin("--tun");
}
diff --git a/Views/TunConfirmationDialog.xaml b/Views/TunConfirmationDialog.xaml
new file mode 100644
index 0000000..370d775
--- /dev/null
+++ b/Views/TunConfirmationDialog.xaml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Views/TunConfirmationDialog.xaml.cs b/Views/TunConfirmationDialog.xaml.cs
new file mode 100644
index 0000000..d4e5580
--- /dev/null
+++ b/Views/TunConfirmationDialog.xaml.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.NetworkInformation;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using XrayUI.Services;
+
+namespace XrayUI.Views
+{
+ public sealed partial class TunConfirmationDialog : UserControl
+ {
+ private const string AutoInterfaceLabel = "auto(推荐)";
+
+ public TunConfirmationDialog(int currentMtu, string currentInterface)
+ {
+ this.InitializeComponent();
+ MtuNumberBox.Value = currentMtu;
+ PopulateInterfaceComboBox(currentInterface);
+ }
+
+ public int Mtu => XrayConfigConstants.NormalizeTunMtu(
+ double.IsNaN(MtuNumberBox.Value) ? XrayConfigConstants.TunMtuDefault : (int)MtuNumberBox.Value);
+
+ public string SelectedInterface =>
+ (InterfaceComboBox.SelectedItem as ComboBoxItem)?.Tag as string
+ ?? XrayConfigConstants.TunOutboundInterfaceAuto;
+
+ private void MoreOptionsButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (AdvancedSettingsPanel.Visibility == Visibility.Visible)
+ {
+ AdvancedSettingsPanel.Visibility = Visibility.Collapsed;
+ MoreOptionsButton.Content = "更多选项";
+ }
+ else
+ {
+ AdvancedSettingsPanel.Visibility = Visibility.Visible;
+ }
+ }
+
+ private void PopulateInterfaceComboBox(string selectedInterface)
+ {
+ InterfaceComboBox.Items.Clear();
+
+ var autoItem = new ComboBoxItem
+ {
+ Content = AutoInterfaceLabel,
+ Tag = XrayConfigConstants.TunOutboundInterfaceAuto,
+ };
+ InterfaceComboBox.Items.Add(autoItem);
+
+ ComboBoxItem? matchingItem = null;
+ foreach (var name in EnumerateActiveInterfaceNames())
+ {
+ var item = new ComboBoxItem { Content = name, Tag = name };
+ InterfaceComboBox.Items.Add(item);
+ if (string.Equals(name, selectedInterface, StringComparison.OrdinalIgnoreCase))
+ matchingItem = item;
+ }
+
+ // The persisted interface may no longer be present (Wi-Fi adapter removed,
+ // VPN uninstalled, etc.) — surface it anyway so the user sees what's saved
+ // and can change it deliberately.
+ if (matchingItem is null
+ && !string.IsNullOrWhiteSpace(selectedInterface)
+ && !string.Equals(selectedInterface, XrayConfigConstants.TunOutboundInterfaceAuto, StringComparison.OrdinalIgnoreCase))
+ {
+ matchingItem = new ComboBoxItem { Content = selectedInterface, Tag = selectedInterface };
+ InterfaceComboBox.Items.Add(matchingItem);
+ }
+
+ InterfaceComboBox.SelectedItem = matchingItem ?? autoItem;
+ }
+
+ private static List EnumerateActiveInterfaceNames()
+ {
+ try
+ {
+ return NetworkInterface.GetAllNetworkInterfaces()
+ .Where(ni => ni.OperationalStatus == OperationalStatus.Up
+ && ni.NetworkInterfaceType != NetworkInterfaceType.Loopback)
+ .Select(ni => ni.Name)
+ .OrderBy(name => name)
+ .ToList();
+ }
+ catch
+ {
+ return [];
+ }
+ }
+ }
+}
diff --git a/XrayUI-dev.csproj b/XrayUI-dev.csproj
index 22983de..37c77d8 100644
--- a/XrayUI-dev.csproj
+++ b/XrayUI-dev.csproj
@@ -72,11 +72,11 @@
strip their runtime assets (DirectML.dll / onnxruntime.dll / Microsoft.Windows.AI.MachineLearning.dll).
IncludeAssets=compile keeps the reference assemblies for compile-time use; PrivateAssets=all
prevents them from flowing to consumers. -->
-
+
all
compile
-
+
all
compile