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);