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,
|
NetworkInterfaceType.FastEthernetT,
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly string[] _allowedPhysicalNameKeywords =
|
private static readonly string[] _allowedPhysicalNamePrefixes =
|
||||||
[
|
[
|
||||||
// Windows / Generic
|
|
||||||
"ethernet", "wlan", "wi-fi", "wifi",
|
|
||||||
|
|
||||||
// Linux
|
// Linux
|
||||||
"eth", "enp", "ens", "eno", "enx", "wlp", "wlan",
|
"eth", "enp", "ens", "eno", "enx", "wlp", "wlan",
|
||||||
|
|
||||||
// macOS
|
// macOS
|
||||||
"en0", "en1", "en2", "airport",
|
"en0", "en1", "en2",
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly string[] _allowedPhysicalNameKeywords =
|
||||||
|
[
|
||||||
|
// Windows / macOS / Generic
|
||||||
|
"ethernet", "wlan", "wi-fi", "wifi", "airport",
|
||||||
|
|
||||||
// Chinese
|
// Chinese
|
||||||
"以太网", "无线", "局域网"
|
"以太网", "无线", "局域网"
|
||||||
@@ -31,14 +34,74 @@ public static class NetworkUtils
|
|||||||
// Generic
|
// Generic
|
||||||
"virtual", "loopback", "loop back",
|
"virtual", "loopback", "loop back",
|
||||||
|
|
||||||
// Common virtual adapters
|
// Common virtual / VPN / tunnel adapters
|
||||||
"hyper-v", "vmware", "virtualbox", "vbox", "docker", "veth", "virbr", "cni",
|
"hyper-v", "vmware", "virtualbox", "vbox", "docker", "veth", "virbr", "cni",
|
||||||
"flannel", "cali", "kube", "wintun", "wireguard", "zerotier", "tailscale", "utun",
|
"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
|
// 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()
|
public static List<string> GetLocalNetworkInterfaceNames()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -51,15 +114,16 @@ public static class NetworkUtils
|
|||||||
.OrderBy(x => x)
|
.OrderBy(x => x)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
Logging.SaveLog("GetLocalNetworkInterfaceNames", ex);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsAllowedPhysicalInterface(NetworkInterface ni)
|
private static bool IsAllowedPhysicalInterface(NetworkInterface ni)
|
||||||
{
|
{
|
||||||
if (ni.OperationalStatus != OperationalStatus.Up)
|
if (ni.OperationalStatus != OperationalStatus.Up || !SupportsIP(ni))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -75,6 +139,38 @@ public static class NetworkUtils
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_allowedPhysicalNamePrefixes.Any(prefix => ni.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return _allowedPhysicalNameKeywords.Any(keyword => interfaceText.Contains(keyword, StringComparison.OrdinalIgnoreCase));
|
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 MozillaRootProvider = "mozilla";
|
||||||
|
|
||||||
public const string DefaultSecurity = "auto";
|
public const string DefaultSecurity = "auto";
|
||||||
|
public const string BindInterfaceAuto = "AUTO";
|
||||||
public const string DefaultNetwork = "raw";
|
public const string DefaultNetwork = "raw";
|
||||||
public const string RawHeaderHttp = "http";
|
public const string RawHeaderHttp = "http";
|
||||||
public const string None = "none";
|
public const string None = "none";
|
||||||
|
|||||||
@@ -121,17 +121,28 @@ public class CoreConfigContextBuilder
|
|||||||
&& !Utils.IsLocalIP(context.AppConfig.CoreBasicItem.SendThrough))
|
&& !Utils.IsLocalIP(context.AppConfig.CoreBasicItem.SendThrough))
|
||||||
{
|
{
|
||||||
validatorResult.Warnings.Add(string.Format(ResUI.MsgInvalidProperty, ResUI.TbSettingsSendThrough));
|
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;
|
appConfig.CoreBasicItem.SendThrough = string.Empty;
|
||||||
context = context with { AppConfig = appConfig };
|
context = context with { AppConfig = appConfig };
|
||||||
}
|
}
|
||||||
if (!context.AppConfig.CoreBasicItem.BindInterface.IsNullOrEmpty()
|
var bindInterface = context.AppConfig.CoreBasicItem.BindInterface?.TrimEx();
|
||||||
&& (!Utils.ContainsInterfaceName(context.AppConfig.CoreBasicItem.BindInterface)
|
if (bindInterface.IsNotEmpty())
|
||||||
|| !(context.IsTunEnabled || context.IsWindows)))
|
|
||||||
{
|
{
|
||||||
validatorResult.Warnings.Add(string.Format(ResUI.MsgInvalidProperty, ResUI.TbSettingsBindInterface));
|
if (NetworkUtils.IsAutoBindInterface(bindInterface))
|
||||||
var appConfig = JsonUtils.DeepCopy(config);
|
{
|
||||||
appConfig.CoreBasicItem.BindInterface = string.Empty;
|
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 };
|
context = context with { AppConfig = appConfig };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ public partial class CoreConfigSingboxService
|
|||||||
|
|
||||||
private void ApplyOutboundBindInterface()
|
private void ApplyOutboundBindInterface()
|
||||||
{
|
{
|
||||||
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
|
var bindInterface = NetworkUtils.GetRealBindInterfaceName(_config.CoreBasicItem.BindInterface);
|
||||||
if (bindInterface.IsNullOrEmpty())
|
if (bindInterface.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ public partial class CoreConfigV2rayService
|
|||||||
|
|
||||||
private void ApplyOutboundBindInterface()
|
private void ApplyOutboundBindInterface()
|
||||||
{
|
{
|
||||||
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
|
var bindInterface = NetworkUtils.GetRealBindInterfaceName(_config.CoreBasicItem.BindInterface);
|
||||||
if (bindInterface.IsNullOrEmpty())
|
if (bindInterface.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ public partial class CoreConfigV2rayService
|
|||||||
tunInbound.settings.gateway.Add(address6);
|
tunInbound.settings.gateway.Add(address6);
|
||||||
}
|
}
|
||||||
|
|
||||||
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
|
var bindInterface = NetworkUtils.GetRealBindInterfaceName(_config.CoreBasicItem.BindInterface);
|
||||||
if (!bindInterface.IsNullOrEmpty())
|
if (!bindInterface.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
tunInbound.settings.autoOutboundsInterface = bindInterface;
|
tunInbound.settings.autoOutboundsInterface = bindInterface;
|
||||||
|
|||||||
@@ -444,7 +444,7 @@
|
|||||||
x:Name="cmbbindInterface"
|
x:Name="cmbbindInterface"
|
||||||
Grid.Row="22"
|
Grid.Row="22"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Width="300"
|
Width="200"
|
||||||
Margin="{StaticResource Margin4}"
|
Margin="{StaticResource Margin4}"
|
||||||
IsEditable="True" />
|
IsEditable="True" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
|
|||||||
cmbRoutingRulesSourceUrl.ItemsSource = Global.RoutingRulesSources;
|
cmbRoutingRulesSourceUrl.ItemsSource = Global.RoutingRulesSources;
|
||||||
cmbIPAPIUrl.ItemsSource = Global.IPAPIUrls;
|
cmbIPAPIUrl.ItemsSource = Global.IPAPIUrls;
|
||||||
cmbRootCertificateProvider.ItemsSource = Global.RootCertProviders;
|
cmbRootCertificateProvider.ItemsSource = Global.RootCertProviders;
|
||||||
cmbbindInterface.ItemsSource = NetworkUtils.GetLocalNetworkInterfaceNames().AppendEmpty();
|
cmbbindInterface.ItemsSource = NetworkUtils.GetBindInterfaceNames();
|
||||||
|
|
||||||
cmbMainGirdOrientation.ItemsSource = Utils.GetEnumNames<EGirdOrientation>();
|
cmbMainGirdOrientation.ItemsSource = Utils.GetEnumNames<EGirdOrientation>();
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public partial class OptionSettingWindow
|
|||||||
cmbRoutingRulesSourceUrl.ItemsSource = Global.RoutingRulesSources;
|
cmbRoutingRulesSourceUrl.ItemsSource = Global.RoutingRulesSources;
|
||||||
cmbIPAPIUrl.ItemsSource = Global.IPAPIUrls;
|
cmbIPAPIUrl.ItemsSource = Global.IPAPIUrls;
|
||||||
cmbRootCertificateProvider.ItemsSource = Global.RootCertProviders;
|
cmbRootCertificateProvider.ItemsSource = Global.RootCertProviders;
|
||||||
txtbindInterface.ItemsSource = NetworkUtils.GetLocalNetworkInterfaceNames().AppendEmpty();
|
txtbindInterface.ItemsSource = NetworkUtils.GetBindInterfaceNames();
|
||||||
|
|
||||||
cmbMainGirdOrientation.ItemsSource = Utils.GetEnumNames<EGirdOrientation>();
|
cmbMainGirdOrientation.ItemsSource = Utils.GetEnumNames<EGirdOrientation>();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user