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
7 changes: 5 additions & 2 deletions Helpers/L.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
3 changes: 3 additions & 0 deletions Models/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ namespace XrayUI.Models
public class AppSettings
{
public int LocalMixedPort { get; set; } = 16890;
/// <summary>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.</summary>
public bool AllowLanConnections { get; set; } = false;
/// <summary>"smart" | "global"</summary>
public string RoutingMode { get; set; } = "smart";
/// <summary>Domestic region treated as direct in smart routing: "cn" | "ru" | "ir".
Expand Down
81 changes: 77 additions & 4 deletions Services/DialogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -489,7 +490,7 @@ void UpdateVisibility()

// ── Edit local port ───────────────────────────────────────────────────

public async Task<int?> ShowEditPortDialogAsync(int currentPort)
public async Task<(int port, bool allowLan)?> ShowEditPortDialogAsync(int currentPort, bool currentAllowLan)
{
var numBox = new NumberBox
{
Expand All @@ -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;
Expand All @@ -508,22 +579,24 @@ void UpdateVisibility()
dialog.Content = new StackPanel
{
Width = 260,
Spacing = 8,
Spacing = 12,
Children =
{
numBox,
new TextBlock
{
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 ─────────────────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion Services/IDialogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public interface IDialogService
Task<SubscriptionEntry?> ShowSubscriptionsDialogAsync(ManageSubscriptionsViewModel vm);
Task<ServerEntry?> ShowEditServerDialogAsync(ServerEntry? existing);
Task<ServerEntry?> ShowChainProxyDialogAsync(IEnumerable<ServerEntry> servers, ServerEntry? existing = null);
Task<int?> ShowEditPortDialogAsync(int currentPort);
Task<(int port, bool allowLan)?> ShowEditPortDialogAsync(int currentPort, bool currentAllowLan);
Task ShowErrorAsync(string title, string message, XamlRoot? xamlRoot = null);
Task<bool> ShowConfirmationAsync(string title, string message, string? confirmText = null, string? cancelText = null, bool isDanger = false);
/// <summary>
Expand Down
56 changes: 56 additions & 0 deletions Services/TunService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,62 @@ private static bool HasIPv4Gateway(NetworkInterface networkInterface) =>
gateway.Address.AddressFamily == AddressFamily.InterNetwork
&& !gateway.Address.Equals(IPAddress.Any));

/// <summary>Best-effort LAN-facing IPv4 address for display in the edit-port dialog's LAN
/// sharing UI (not for outbound pinning — see <see cref="ResolveOutboundInterface"/> for that).
/// Reuses the same adapter filter (<see cref="IsTunLikeInterface"/>, <see cref="HasIPv4Gateway"/>)
/// 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.</summary>
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
}

/// <summary>
/// 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
Expand Down
2 changes: 1 addition & 1 deletion Services/XrayConfigBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid binding LAN sharing to every interface

When Allow LAN is enabled on a host where Windows Firewall allows xray.exe and the machine also has a VPN/public adapter, 0.0.0.0 publishes the no-auth inbound on those interfaces as well, not just the LAN address shown in the dialog. That lets peers outside the intended local subnet use the proxy. Please bind a separate LAN inbound to the selected local address while keeping loopback for local use, or make the all-interface exposure explicit.

Useful? React with 👍 / 👎.

["port"] = settings.LocalMixedPort,
["settings"] = new JsonObject
{
Expand Down
12 changes: 12 additions & 0 deletions Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,18 @@
<data name="EditPort_Range" xml:space="preserve">
<value>Range: {0} - {1}</value>
</data>
<data name="EditPort_AllowLan" xml:space="preserve">
<value>Allow LAN connections</value>
</data>
<data name="EditPort_LanAddress" xml:space="preserve">
<value>LAN address: {0}</value>
</data>
<data name="EditPort_LanUnavailable" xml:space="preserve">
<value>No LAN network detected</value>
</data>
<data name="EditPort_CopyAddress" xml:space="preserve">
<value>Copy Address</value>
</data>
<data name="Share_Title" xml:space="preserve">
<value>Share Node</value>
</data>
Expand Down
12 changes: 12 additions & 0 deletions Strings/zh-CN/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,18 @@
<data name="EditPort_Range" xml:space="preserve">
<value>有效范围:{0} - {1}</value>
</data>
<data name="EditPort_AllowLan" xml:space="preserve">
<value>允许局域网设备连接</value>
</data>
<data name="EditPort_LanAddress" xml:space="preserve">
<value>局域网地址:{0}</value>
</data>
<data name="EditPort_LanUnavailable" xml:space="preserve">
<value>未检测到可用的局域网网络</value>
</data>
<data name="EditPort_CopyAddress" xml:space="preserve">
<value>复制地址</value>
</data>
<data name="Share_Title" xml:space="preserve">
<value>分享节点</value>
</data>
Expand Down
27 changes: 21 additions & 6 deletions ViewModels/ControlPanelViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,10 @@ private async Task<bool> 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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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}"); }
}
}
}

Expand Down
1 change: 1 addition & 0 deletions ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading