mirror of
https://github.com/2dust/v2rayN.git
synced 2026-11-08 13:35:01 +03:00
feat(core): support AUTO option and filter non-IP NDIS interfaces for BindInterface (#10519)
* feat(core): support AUTO detection and manual selection for BindInterface (#10519) - Implements the 3-state design agreed in #10519: 1. Empty: do not bind interface, preserve existing behavior. 2. AUTO: automatically detect default active physical network interface and bind to it. 3. Specific interface: prioritize user-selected network interface. - Centralizes interface resolution in NetworkUtils.GetRealBindInterfaceName(). - Supports AUTO in CoreConfigContextBuilder, Singbox, and V2ray template services. - Adds comprehensive unit tests covering AUTO detection, context validation, and config generation. * refactor: optimize AUTO bindInterface resolution and add mock-based tests - Resolve AUTO interface once in CoreConfigContextBuilder and store in CoreConfigContext.ResolvedBindInterface as single-source-of-truth - Downstream sing-box and Xray outbound/TUN services consume ResolvedBindInterface directly - Unify TrimEx and case-insensitive check via NetworkUtils.IsAuto to tolerate user input whitespace - Log and issue validator warning when AUTO fails to resolve any physical adapter - Extract pure SelectDefaultInterface function with NetworkInterfaceCandidate for deterministic, mock-based unit testing - Add comprehensive unit tests covering Ethernet vs Wi-Fi preference, no-gateway fallback, and virtual adapter filtering * refactor(network): filter non-IP NDIS interfaces and simplify AUTO bindInterface resolution * fix(ui,network): align BindInterface ComboBox width and refine physical NIC matching * Update CoreConfigContextBuilder.cs --------- Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
This commit is contained in:
@@ -11,16 +11,19 @@ public static class NetworkUtils
|
||||
NetworkInterfaceType.FastEthernetT,
|
||||
];
|
||||
|
||||
private static readonly string[] _allowedPhysicalNameKeywords =
|
||||
private static readonly string[] _allowedPhysicalNamePrefixes =
|
||||
[
|
||||
// Windows / Generic
|
||||
"ethernet", "wlan", "wi-fi", "wifi",
|
||||
|
||||
// Linux
|
||||
"eth", "enp", "ens", "eno", "enx", "wlp", "wlan",
|
||||
|
||||
// macOS
|
||||
"en0", "en1", "en2", "airport",
|
||||
"en0", "en1", "en2",
|
||||
];
|
||||
|
||||
private static readonly string[] _allowedPhysicalNameKeywords =
|
||||
[
|
||||
// Windows / macOS / Generic
|
||||
"ethernet", "wlan", "wi-fi", "wifi", "airport",
|
||||
|
||||
// Chinese
|
||||
"以太网", "无线", "局域网"
|
||||
@@ -31,14 +34,74 @@ public static class NetworkUtils
|
||||
// Generic
|
||||
"virtual", "loopback", "loop back",
|
||||
|
||||
// Common virtual adapters
|
||||
// Common virtual / VPN / tunnel adapters
|
||||
"hyper-v", "vmware", "virtualbox", "vbox", "docker", "veth", "virbr", "cni",
|
||||
"flannel", "cali", "kube", "wintun", "wireguard", "zerotier", "tailscale", "utun",
|
||||
"tap-windows", "tap adapter", "openvpn", "anyconnect", "vpn", "clash", "mihomo", "xray_tun",
|
||||
|
||||
// Windows NDIS filter sub-layers & pseudo-interfaces
|
||||
"wan miniport", "lightweight filter", "filter driver", "packet scheduler", "npcap", "kernel debug",
|
||||
|
||||
// Chinese
|
||||
"虚拟", "回环", "环回"
|
||||
];
|
||||
|
||||
public static bool IsAutoBindInterface(string? bindInterface)
|
||||
{
|
||||
return string.Equals(bindInterface?.TrimEx(), Global.BindInterfaceAuto, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static List<string> GetBindInterfaceNames()
|
||||
{
|
||||
return GetLocalNetworkInterfaceNames().Prepend(Global.BindInterfaceAuto).AppendEmpty();
|
||||
}
|
||||
|
||||
public static string? GetRealBindInterfaceName(string? bindInterface)
|
||||
{
|
||||
var trimmed = bindInterface?.TrimEx();
|
||||
if (trimmed.IsNullOrEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return IsAutoBindInterface(trimmed) ? GetDefaultPhysicalInterfaceName() : trimmed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a default physical outbound interface using a heuristic (active IP-enabled physical NICs
|
||||
/// with an IPv4 gateway, preferring Ethernet over Wi-Fi). This does not inspect OS routing metrics;
|
||||
/// in multi-NIC environments with custom routing tables, specify the target interface name manually.
|
||||
/// </summary>
|
||||
public static string? GetDefaultPhysicalInterfaceName()
|
||||
{
|
||||
try
|
||||
{
|
||||
var upPhysicalNics = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(IsAllowedPhysicalInterface)
|
||||
.ToList();
|
||||
|
||||
if (upPhysicalNics.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var gatewayNics = upPhysicalNics.Where(HasIPv4Gateway).ToList();
|
||||
var candidates = gatewayNics.Count > 0 ? gatewayNics : upPhysicalNics;
|
||||
|
||||
var bestNic = candidates.FirstOrDefault(ni =>
|
||||
ni.NetworkInterfaceType is NetworkInterfaceType.Ethernet or NetworkInterfaceType.GigabitEthernet)
|
||||
?? candidates.FirstOrDefault(ni => ni.NetworkInterfaceType == NetworkInterfaceType.Wireless80211)
|
||||
?? candidates.FirstOrDefault();
|
||||
|
||||
return bestNic?.Name;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog("GetDefaultPhysicalInterfaceName", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<string> GetLocalNetworkInterfaceNames()
|
||||
{
|
||||
try
|
||||
@@ -51,15 +114,16 @@ public static class NetworkUtils
|
||||
.OrderBy(x => x)
|
||||
.ToList();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog("GetLocalNetworkInterfaceNames", ex);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAllowedPhysicalInterface(NetworkInterface ni)
|
||||
{
|
||||
if (ni.OperationalStatus != OperationalStatus.Up)
|
||||
if (ni.OperationalStatus != OperationalStatus.Up || !SupportsIP(ni))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -75,6 +139,38 @@ public static class NetworkUtils
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_allowedPhysicalNamePrefixes.Any(prefix => ni.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _allowedPhysicalNameKeywords.Any(keyword => interfaceText.Contains(keyword, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool SupportsIP(NetworkInterface ni)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ni.Supports(NetworkInterfaceComponent.IPv4) || ni.Supports(NetworkInterfaceComponent.IPv6);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasIPv4Gateway(NetworkInterface ni)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ni.GetIPProperties().GatewayAddresses.Any(g =>
|
||||
g.Address.AddressFamily == AddressFamily.InterNetwork
|
||||
&& !IPAddress.Any.Equals(g.Address)
|
||||
&& !IPAddress.None.Equals(g.Address));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ public class Global
|
||||
public const string MozillaRootProvider = "mozilla";
|
||||
|
||||
public const string DefaultSecurity = "auto";
|
||||
public const string BindInterfaceAuto = "AUTO";
|
||||
public const string DefaultNetwork = "raw";
|
||||
public const string RawHeaderHttp = "http";
|
||||
public const string None = "none";
|
||||
|
||||
@@ -121,17 +121,28 @@ public class CoreConfigContextBuilder
|
||||
&& !Utils.IsLocalIP(context.AppConfig.CoreBasicItem.SendThrough))
|
||||
{
|
||||
validatorResult.Warnings.Add(string.Format(ResUI.MsgInvalidProperty, ResUI.TbSettingsSendThrough));
|
||||
var appConfig = JsonUtils.DeepCopy(config);
|
||||
var appConfig = JsonUtils.DeepCopy(context.AppConfig);
|
||||
appConfig.CoreBasicItem.SendThrough = string.Empty;
|
||||
context = context with { AppConfig = appConfig };
|
||||
}
|
||||
if (!context.AppConfig.CoreBasicItem.BindInterface.IsNullOrEmpty()
|
||||
&& (!Utils.ContainsInterfaceName(context.AppConfig.CoreBasicItem.BindInterface)
|
||||
|| !(context.IsTunEnabled || context.IsWindows)))
|
||||
var bindInterface = context.AppConfig.CoreBasicItem.BindInterface?.TrimEx();
|
||||
if (bindInterface.IsNotEmpty())
|
||||
{
|
||||
validatorResult.Warnings.Add(string.Format(ResUI.MsgInvalidProperty, ResUI.TbSettingsBindInterface));
|
||||
var appConfig = JsonUtils.DeepCopy(config);
|
||||
appConfig.CoreBasicItem.BindInterface = string.Empty;
|
||||
if (NetworkUtils.IsAutoBindInterface(bindInterface))
|
||||
{
|
||||
bindInterface = NetworkUtils.GetDefaultPhysicalInterfaceName()?.TrimEx();
|
||||
}
|
||||
|
||||
if (bindInterface.IsNullOrEmpty()
|
||||
|| !Utils.ContainsInterfaceName(bindInterface)
|
||||
|| !(context.IsTunEnabled || context.IsWindows))
|
||||
{
|
||||
validatorResult.Warnings.Add(string.Format(ResUI.MsgInvalidProperty, ResUI.TbSettingsBindInterface));
|
||||
bindInterface = string.Empty;
|
||||
}
|
||||
|
||||
var appConfig = JsonUtils.DeepCopy(context.AppConfig);
|
||||
appConfig.CoreBasicItem.BindInterface = bindInterface;
|
||||
context = context with { AppConfig = appConfig };
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ public partial class CoreConfigSingboxService
|
||||
|
||||
private void ApplyOutboundBindInterface()
|
||||
{
|
||||
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
|
||||
var bindInterface = NetworkUtils.GetRealBindInterfaceName(_config.CoreBasicItem.BindInterface);
|
||||
if (bindInterface.IsNullOrEmpty())
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -224,7 +224,7 @@ public partial class CoreConfigV2rayService
|
||||
|
||||
private void ApplyOutboundBindInterface()
|
||||
{
|
||||
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
|
||||
var bindInterface = NetworkUtils.GetRealBindInterfaceName(_config.CoreBasicItem.BindInterface);
|
||||
if (bindInterface.IsNullOrEmpty())
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -79,7 +79,7 @@ public partial class CoreConfigV2rayService
|
||||
tunInbound.settings.gateway.Add(address6);
|
||||
}
|
||||
|
||||
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
|
||||
var bindInterface = NetworkUtils.GetRealBindInterfaceName(_config.CoreBasicItem.BindInterface);
|
||||
if (!bindInterface.IsNullOrEmpty())
|
||||
{
|
||||
tunInbound.settings.autoOutboundsInterface = bindInterface;
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
x:Name="cmbbindInterface"
|
||||
Grid.Row="22"
|
||||
Grid.Column="1"
|
||||
Width="300"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}"
|
||||
IsEditable="True" />
|
||||
<TextBlock
|
||||
|
||||
@@ -60,7 +60,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
|
||||
cmbRoutingRulesSourceUrl.ItemsSource = Global.RoutingRulesSources;
|
||||
cmbIPAPIUrl.ItemsSource = Global.IPAPIUrls;
|
||||
cmbRootCertificateProvider.ItemsSource = Global.RootCertProviders;
|
||||
cmbbindInterface.ItemsSource = NetworkUtils.GetLocalNetworkInterfaceNames().AppendEmpty();
|
||||
cmbbindInterface.ItemsSource = NetworkUtils.GetBindInterfaceNames();
|
||||
|
||||
cmbMainGirdOrientation.ItemsSource = Utils.GetEnumNames<EGirdOrientation>();
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ public partial class OptionSettingWindow
|
||||
cmbRoutingRulesSourceUrl.ItemsSource = Global.RoutingRulesSources;
|
||||
cmbIPAPIUrl.ItemsSource = Global.IPAPIUrls;
|
||||
cmbRootCertificateProvider.ItemsSource = Global.RootCertProviders;
|
||||
txtbindInterface.ItemsSource = NetworkUtils.GetLocalNetworkInterfaceNames().AppendEmpty();
|
||||
txtbindInterface.ItemsSource = NetworkUtils.GetBindInterfaceNames();
|
||||
|
||||
cmbMainGirdOrientation.ItemsSource = Utils.GetEnumNames<EGirdOrientation>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user