diff --git a/v2rayN/ServiceLib/Common/NetworkUtils.cs b/v2rayN/ServiceLib/Common/NetworkUtils.cs index 209455f4..dd32e0c8 100644 --- a/v2rayN/ServiceLib/Common/NetworkUtils.cs +++ b/v2rayN/ServiceLib/Common/NetworkUtils.cs @@ -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 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; + } + + /// + /// 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. + /// + 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 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; + } + } } diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index 3808f57b..d72ae827 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -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"; diff --git a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs index abce5de6..5d4e8a5c 100644 --- a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs +++ b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs @@ -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 }; } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs index df4117f8..a5d9a2fe 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxConfigTemplateService.cs @@ -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; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs index 50dfe001..9b45a697 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayConfigTemplateService.cs @@ -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; diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs index 0da0c29c..0c89e731 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayInboundService.cs @@ -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; diff --git a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml index 0217f67a..240fd4b5 100644 --- a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml @@ -444,7 +444,7 @@ x:Name="cmbbindInterface" Grid.Row="22" Grid.Column="1" - Width="300" + Width="200" Margin="{StaticResource Margin4}" IsEditable="True" /> 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(); diff --git a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs index c01e2895..b8d05abb 100644 --- a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs +++ b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml.cs @@ -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();