diff --git a/Helpers/L.cs b/Helpers/L.cs
index 858bad4..805529a 100644
--- a/Helpers/L.cs
+++ b/Helpers/L.cs
@@ -51,8 +51,11 @@ public static class L
public static string EditServer_FlowPlaceholder => Loc.GetString("EditServer_FlowPlaceholder");
// ── Edit port dialog ───────────────────────────────────────────────────
- public static string EditPort_Title => Loc.GetString("EditPort_Title");
- public static string EditPort_Header => Loc.GetString("EditPort_Header");
+ public static string EditPort_Title => Loc.GetString("EditPort_Title");
+ public static string EditPort_Header => Loc.GetString("EditPort_Header");
+ public static string EditPort_AllowLan => Loc.GetString("EditPort_AllowLan");
+ public static string EditPort_LanUnavailable => Loc.GetString("EditPort_LanUnavailable");
+ public static string EditPort_CopyAddress => Loc.GetString("EditPort_CopyAddress");
// ── TUN mode ───────────────────────────────────────────────────────────
public static string Tun_EnableTitle => Loc.GetString("Tun_EnableTitle");
diff --git a/Models/AppSettings.cs b/Models/AppSettings.cs
index 7ef4393..eb9cfb9 100644
--- a/Models/AppSettings.cs
+++ b/Models/AppSettings.cs
@@ -7,6 +7,9 @@ namespace XrayUI.Models
public class AppSettings
{
public int LocalMixedPort { get; set; } = 16890;
+ /// When true, the local socks/http inbound listens on 0.0.0.0 instead of
+ /// 127.0.0.1 so other devices on the LAN can use this machine as a proxy.
+ public bool AllowLanConnections { get; set; } = false;
/// "smart" | "global"
public string RoutingMode { get; set; } = "smart";
/// Domestic region treated as direct in smart routing: "cn" | "ru" | "ir".
diff --git a/Services/DialogService.cs b/Services/DialogService.cs
index 4fbbba7..8dbc0cd 100644
--- a/Services/DialogService.cs
+++ b/Services/DialogService.cs
@@ -4,6 +4,7 @@
using System.ComponentModel;
using System.Collections.Generic;
using Windows.ApplicationModel.DataTransfer;
+using XrayUI.Controls;
using XrayUI.Helpers;
using XrayUI.Models;
using XrayUI.Views;
@@ -489,7 +490,7 @@ void UpdateVisibility()
// ── Edit local port ───────────────────────────────────────────────────
- public async Task ShowEditPortDialogAsync(int currentPort)
+ public async Task<(int port, bool allowLan)?> ShowEditPortDialogAsync(int currentPort, bool currentAllowLan)
{
var numBox = new NumberBox
{
@@ -500,6 +501,76 @@ void UpdateVisibility()
SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Inline,
};
+ var lanToggle = new ToggleSwitch
+ {
+ IsOn = currentAllowLan,
+ OnContent = L.Dialog_On,
+ OffContent = L.Dialog_Off,
+ MinWidth = 0,
+ Margin = new Thickness(0),
+ };
+ var lanRow = CreateLabelRow(L.EditPort_AllowLan, lanToggle);
+
+ var lanAddressText = new TextBlock
+ {
+ Opacity = 0.65,
+ TextWrapping = TextWrapping.Wrap,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+
+ var transparentBrush = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Transparent);
+ var lanCopyBtn = new CopyButton
+ {
+ Content = "",
+ FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Segoe Fluent Icons"),
+ Width = 28,
+ Height = 28,
+ Padding = new Thickness(0),
+ FontSize = 14,
+ VerticalAlignment = VerticalAlignment.Center,
+ Background = transparentBrush,
+ BorderBrush = transparentBrush,
+ };
+ ToolTipService.SetToolTip(lanCopyBtn, L.EditPort_CopyAddress);
+
+ var lanAddressRow = new Grid { ColumnSpacing = 4 };
+ lanAddressRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ lanAddressRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ Grid.SetColumn(lanAddressText, 0);
+ Grid.SetColumn(lanCopyBtn, 1);
+ lanAddressRow.Children.Add(lanAddressText);
+ lanAddressRow.Children.Add(lanCopyBtn);
+
+ var lanAddress = TunService.GetLanDisplayAddress();
+ int CurrentPortValue() => double.IsNaN(numBox.Value) ? currentPort : (int)numBox.Value;
+
+ void UpdateLanAddressText()
+ {
+ if (!lanToggle.IsOn)
+ {
+ lanAddressRow.Visibility = Visibility.Collapsed;
+ return;
+ }
+
+ if (lanAddress is not null)
+ {
+ var address = $"{lanAddress}:{CurrentPortValue()}";
+ lanAddressText.Text = Loc.Format("EditPort_LanAddress", address);
+ lanCopyBtn.TextToCopy = address;
+ lanCopyBtn.Visibility = Visibility.Visible;
+ }
+ else
+ {
+ lanAddressText.Text = L.EditPort_LanUnavailable;
+ lanCopyBtn.Visibility = Visibility.Collapsed;
+ }
+ lanAddressRow.Visibility = Visibility.Visible;
+ }
+
+ lanToggle.Toggled += (_, _) => UpdateLanAddressText();
+ numBox.ValueChanged += (_, _) => UpdateLanAddressText();
+ UpdateLanAddressText();
+
var dialog = CreateDialog();
dialog.Title = L.EditPort_Title;
dialog.PrimaryButtonText = L.Dialog_OK;
@@ -508,7 +579,7 @@ void UpdateVisibility()
dialog.Content = new StackPanel
{
Width = 260,
- Spacing = 8,
+ Spacing = 12,
Children =
{
numBox,
@@ -516,14 +587,16 @@ void UpdateVisibility()
{
Text = Loc.Format("EditPort_Range", numBox.Minimum, numBox.Maximum),
Opacity = 0.65,
- }
+ },
+ lanRow,
+ lanAddressRow,
}
};
var result = await dialog.ShowAsync();
if (result != ContentDialogResult.Primary) return null;
- return double.IsNaN(numBox.Value) ? currentPort : (int)numBox.Value;
+ return (CurrentPortValue(), lanToggle.IsOn);
}
// ── Error ─────────────────────────────────────────────────────────────
diff --git a/Services/IDialogService.cs b/Services/IDialogService.cs
index b623cd0..3466f21 100644
--- a/Services/IDialogService.cs
+++ b/Services/IDialogService.cs
@@ -12,7 +12,7 @@ public interface IDialogService
Task ShowSubscriptionsDialogAsync(ManageSubscriptionsViewModel vm);
Task ShowEditServerDialogAsync(ServerEntry? existing);
Task ShowChainProxyDialogAsync(IEnumerable servers, ServerEntry? existing = null);
- Task ShowEditPortDialogAsync(int currentPort);
+ Task<(int port, bool allowLan)?> ShowEditPortDialogAsync(int currentPort, bool currentAllowLan);
Task ShowErrorAsync(string title, string message, XamlRoot? xamlRoot = null);
Task ShowConfirmationAsync(string title, string message, string? confirmText = null, string? cancelText = null, bool isDanger = false);
///
diff --git a/Services/TunService.cs b/Services/TunService.cs
index 47eee5d..1021dd8 100644
--- a/Services/TunService.cs
+++ b/Services/TunService.cs
@@ -135,6 +135,62 @@ private static bool HasIPv4Gateway(NetworkInterface networkInterface) =>
gateway.Address.AddressFamily == AddressFamily.InterNetwork
&& !gateway.Address.Equals(IPAddress.Any));
+ /// Best-effort LAN-facing IPv4 address for display in the edit-port dialog's LAN
+ /// sharing UI (not for outbound pinning — see for that).
+ /// Reuses the same adapter filter (, )
+ /// so "what counts as a real LAN adapter" stays defined in one place. Prefers an adapter with a
+ /// real IPv4 gateway; a single gateway-less adapter is still accepted since it can be a valid
+ /// isolated LAN, but with several such adapters there is no reliable way to distinguish the
+ /// physical network from Hyper-V/VM host-only switches, so none is returned.
+ internal static string? GetLanDisplayAddress()
+ {
+ try
+ {
+ string? gatewayed = null;
+ string? soleFallback = null;
+ var fallbackCount = 0;
+
+ foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
+ {
+ if (ni.OperationalStatus != OperationalStatus.Up || IsTunLikeInterface(ni))
+ continue;
+
+ var address = ni.GetIPProperties().UnicastAddresses
+ .Select(a => a.Address)
+ .FirstOrDefault(IsUsableLanIPv4Address);
+ if (address is null)
+ continue;
+
+ if (HasIPv4Gateway(ni))
+ gatewayed ??= address.ToString();
+ else
+ {
+ fallbackCount++;
+ soleFallback = address.ToString();
+ }
+ }
+
+ return gatewayed ?? (fallbackCount == 1 ? soleFallback : null);
+ }
+ catch (NetworkInformationException)
+ {
+ return null;
+ }
+ }
+
+ private static bool IsUsableLanIPv4Address(IPAddress address)
+ {
+ if (address.AddressFamily != AddressFamily.InterNetwork
+ || address.Equals(IPAddress.Any)
+ || IPAddress.IsLoopback(address))
+ {
+ return false;
+ }
+
+ var bytes = address.GetAddressBytes();
+ return bytes[0] != 169 || bytes[1] != 254; // exclude APIPA/link-local addresses
+ }
+
///
/// 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
diff --git a/Services/XrayConfigBuilder.cs b/Services/XrayConfigBuilder.cs
index 422fdf8..d9696b0 100644
--- a/Services/XrayConfigBuilder.cs
+++ b/Services/XrayConfigBuilder.cs
@@ -97,7 +97,7 @@ private static JsonArray BuildInbounds(AppSettings settings)
{
["tag"] = XrayConfigConstants.MixedInboundTag,
["protocol"] = "socks",
- ["listen"] = "127.0.0.1",
+ ["listen"] = settings.AllowLanConnections ? "0.0.0.0" : "127.0.0.1",
["port"] = settings.LocalMixedPort,
["settings"] = new JsonObject
{
diff --git a/Strings/en-US/Resources.resw b/Strings/en-US/Resources.resw
index 21fc4b9..5ce9a1e 100644
--- a/Strings/en-US/Resources.resw
+++ b/Strings/en-US/Resources.resw
@@ -455,6 +455,18 @@
Range: {0} - {1}
+
+ Allow LAN connections
+
+
+ LAN address: {0}
+
+
+ No LAN network detected
+
+
+ Copy Address
+
Share Node
diff --git a/Strings/zh-CN/Resources.resw b/Strings/zh-CN/Resources.resw
index 5632bdc..aa5b40a 100644
--- a/Strings/zh-CN/Resources.resw
+++ b/Strings/zh-CN/Resources.resw
@@ -455,6 +455,18 @@
有效范围:{0} - {1}
+
+ 允许局域网设备连接
+
+
+ 局域网地址:{0}
+
+
+ 未检测到可用的局域网网络
+
+
+ 复制地址
+
分享节点
diff --git a/ViewModels/ControlPanelViewModel.cs b/ViewModels/ControlPanelViewModel.cs
index a792a42..64b5014 100644
--- a/ViewModels/ControlPanelViewModel.cs
+++ b/ViewModels/ControlPanelViewModel.cs
@@ -201,9 +201,10 @@ private async Task StartSelectedServerAsync()
}
var appSettings = await _settings.LoadSettingsAsync();
- appSettings.LocalMixedPort = LocalPort;
- appSettings.RoutingMode = RoutingMode;
- appSettings.IsTunMode = IsTunMode;
+ appSettings.LocalMixedPort = LocalPort;
+ appSettings.AllowLanConnections = AllowLanConnections;
+ appSettings.RoutingMode = RoutingMode;
+ appSettings.IsTunMode = IsTunMode;
if (IsAutoConnect)
appSettings.LastAutoConnectServerId = server.Id;
@@ -290,6 +291,7 @@ public async Task ReapplyRoutingAsync()
{
var settings = await _settings.LoadSettingsAsync();
settings.LocalMixedPort = LocalPort;
+ settings.AllowLanConnections = AllowLanConnections;
settings.RoutingMode = RoutingMode;
settings.IsTunMode = IsTunMode;
settings.IsSystemProxyEnabled = IsSystemProxyEnabled;
@@ -601,18 +603,31 @@ public void SetTunEnabledSilently(bool value)
[NotifyPropertyChangedFor(nameof(LocalPortText))]
public partial int LocalPort { get; set; }
+ [ObservableProperty]
+ public partial bool AllowLanConnections { get; set; }
+
public string LocalPortText => $":{LocalPort}";
[RelayCommand]
private async Task EditLocalPort()
{
- var newPort = await _dialogs.ShowEditPortDialogAsync(LocalPort);
- if (newPort.HasValue)
+ var result = await _dialogs.ShowEditPortDialogAsync(LocalPort, AllowLanConnections);
+ if (result.HasValue && (result.Value.port != LocalPort || result.Value.allowLan != AllowLanConnections))
{
- LocalPort = newPort.Value;
+ LocalPort = result.Value.port;
+ AllowLanConnections = result.Value.allowLan;
var settings = await _settings.LoadSettingsAsync();
settings.LocalMixedPort = LocalPort;
+ settings.AllowLanConnections = AllowLanConnections;
await TrySaveSettingsAsync(settings, "persist local port");
+
+ // Apply live if xray is currently running (no-op in TUN mode, same as
+ // routing/DNS changes — takes effect on the next connect there).
+ if (IsRunning)
+ {
+ try { await ReapplyRoutingAsync(); }
+ catch (Exception ex) { Debug.WriteLine($"[ControlPanel] Reapply after port/LAN change failed: {ex.Message}"); }
+ }
}
}
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
index 2245cd5..0ce3b7c 100644
--- a/ViewModels/MainViewModel.cs
+++ b/ViewModels/MainViewModel.cs
@@ -128,6 +128,7 @@ public async Task InitializeAsync(bool isBootLaunch = false)
// Load settings and apply to ControlPanel
var s = await _settings.LoadSettingsAsync();
ControlPanel.LocalPort = s.LocalMixedPort;
+ ControlPanel.AllowLanConnections = s.AllowLanConnections;
ControlPanel.RoutingMode = s.RoutingMode;
ControlPanel.IsSystemProxyEnabled = s.IsSystemProxyEnabled;
ControlPanel.InitializePersonalize(s);