diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index f7a163d3..572d3e7d 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -119,6 +119,7 @@ public static class ConfigHandler config.SimpleDNSItem.BootstrapDNS ??= Global.DomainPureIPDNSAddress.FirstOrDefault(); config.SimpleDNSItem.ServeStale ??= false; config.SimpleDNSItem.ParallelQuery ??= false; + config.SimpleDNSItem.EnableHappyEyeballs ??= false; config.SpeedTestItem ??= new(); if (config.SpeedTestItem.SpeedTestTimeout < 10) @@ -170,6 +171,14 @@ public static class ConfigHandler Packets = "tlshello", }; config.Fragment4RayItem.MaxSplit ??= "0"; + + config.HappyEyeballs4RayItem ??= new() + { + TryDelayMs = 250, + PrioritizeIPv6 = false, + Interleave = 1, + MaxConcurrentTry = 4, + }; if ((config.Fragment4RayItem.Lengths ?? []).Count == 0) { config.Fragment4RayItem.Lengths = [config.Fragment4RayItem.Length ?? "50-100"]; diff --git a/v2rayN/ServiceLib/Models/Configs/Config.cs b/v2rayN/ServiceLib/Models/Configs/Config.cs index 69770d3c..834a13a9 100644 --- a/v2rayN/ServiceLib/Models/Configs/Config.cs +++ b/v2rayN/ServiceLib/Models/Configs/Config.cs @@ -34,6 +34,7 @@ public class Config public List GlobalHotkeys { get; set; } public List CoreTypeItem { get; set; } public SimpleDNSItem SimpleDNSItem { get; set; } + public HappyEyeballs4RayItem HappyEyeballs4RayItem { get; set; } #endregion other entities } diff --git a/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs b/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs index 4c5570a1..4d44f5c3 100644 --- a/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs @@ -285,4 +285,14 @@ public class SimpleDNSItem public bool? ParallelQuery { get; set; } public string? Hosts { get; set; } public string? DirectExpectedIPs { get; set; } + public bool? EnableHappyEyeballs { get; set; } +} + +[Serializable] +public class HappyEyeballs4RayItem +{ + public int? TryDelayMs { get; set; } + public bool? PrioritizeIPv6 { get; set; } + public int? Interleave { get; set; } + public int? MaxConcurrentTry { get; set; } } diff --git a/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs index d13518e9..dd2d6855 100644 --- a/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs @@ -553,11 +553,13 @@ public class Sockopt4Ray [JsonPropertyName("interface")] public string? Interface { get; set; } + public HappyEyeballs4Ray? happyEyeballs { get; set; } } -public class FragmentItem4Ray +public class HappyEyeballs4Ray { - public string? packets { get; set; } - public string? length { get; set; } - public string? interval { get; set; } + public int? tryDelayMs { get; set; } + public bool? prioritizeIPv6 { get; set; } + public int? interleave { get; set; } + public int? maxConcurrentTry { get; set; } } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 33308576..28f4d92e 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -3060,6 +3060,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Enable Happy Eyeballs 的本地化字符串。 + /// + public static string TbEnableHappyEyeballs { + get { + return ResourceManager.GetString("TbEnableHappyEyeballs", resourceCulture); + } + } + + /// + /// 查找类似 Requires the UseIP Strategy. When enabled, it attempts IPv4 and IPv6 connections simultaneously and automatically selects the faster available path. 的本地化字符串。 + /// + public static string TbEnableHappyEyeballsTip { + get { + return ResourceManager.GetString("TbEnableHappyEyeballsTip", resourceCulture); + } + } + /// /// 查找类似 DNS via Bridge 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 5ddf108d..544f9a9f 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1836,4 +1836,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Not recommended; may cause routing loops. - + + Enable Happy Eyeballs + + + Requires the UseIP Strategy. When enabled, it attempts IPv4 and IPv6 connections simultaneously and automatically selects the faster available path. + + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index 3ae2b752..0ef0e4ce 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1833,4 +1833,10 @@ 不建议开启,特殊情况可能回环。 - + + 启用 Happy Eyeballs + + + 需配合 UseIP 策略使用。启用后将同时尝试 IPv4 和 IPv6 连接,并自动选择更快可用的路径。 + + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs index 7a020096..1cae354b 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayDnsService.cs @@ -43,9 +43,7 @@ public partial class CoreConfigV2rayService var outbound = _coreConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag }); if (outbound != null) { - outbound.streamSettings ??= new(); - outbound.streamSettings.sockopt ??= new(); - outbound.streamSettings.sockopt.domainStrategy = strategy4Freedom; + FillSockoptDomainStrategy(outbound, strategy4Freedom); } } @@ -74,9 +72,7 @@ public partial class CoreConfigV2rayService .ToList() .ForEach(outbound => { - outbound.streamSettings ??= new(); - outbound.streamSettings.sockopt ??= new(); - outbound.streamSettings.sockopt.domainStrategy = strategy4DialProxy; + FillSockoptDomainStrategy(outbound, strategy4DialProxy); }); } @@ -485,4 +481,28 @@ public partial class CoreConfigV2rayService }; servers.AsArray().Add(JsonUtils.SerializeToNode(dnsServer)); } + + private void FillSockoptDomainStrategy(Outbounds4Ray outbound, string? domainStrategy, bool skipHappyEyeballs = false) + { + if (domainStrategy.IsNullOrEmpty()) + { + return; + } + outbound.streamSettings ??= new(); + outbound.streamSettings.sockopt ??= new(); + var sockopt = outbound.streamSettings.sockopt; + sockopt.domainStrategy = domainStrategy; + + if (skipHappyEyeballs || _config.SimpleDNSItem.EnableHappyEyeballs != true) + { + return; + } + var happyEyeballsItem = _config.HappyEyeballs4RayItem; + sockopt.happyEyeballs ??= new(); + var happyEyeballs = sockopt.happyEyeballs; + happyEyeballs.tryDelayMs = happyEyeballsItem.TryDelayMs; + happyEyeballs.prioritizeIPv6 = happyEyeballsItem.PrioritizeIPv6; + happyEyeballs.interleave = happyEyeballsItem.Interleave; + happyEyeballs.maxConcurrentTry = happyEyeballsItem.MaxConcurrentTry; + } } diff --git a/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs index d576f31d..6f7f9f98 100644 --- a/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs @@ -4,20 +4,21 @@ public class DNSSettingViewModel : MyReactiveObject, ICloseable { public event EventHandler? RequestClose; - [Reactive] public bool? UseSystemHosts { get; set; } - [Reactive] public bool? AddCommonHosts { get; set; } - [Reactive] public bool? FakeIP { get; set; } - [Reactive] public bool? BlockBindingQuery { get; set; } - [Reactive] public string? DirectDNS { get; set; } - [Reactive] public string? RemoteDNS { get; set; } - [Reactive] public string? BootstrapDNS { get; set; } - [Reactive] public string? Strategy4Freedom { get; set; } - [Reactive] public string? Strategy4Proxy { get; set; } - [Reactive] public string? Strategy4ProxyDial { get; set; } - [Reactive] public string? Hosts { get; set; } - [Reactive] public string? DirectExpectedIPs { get; set; } - [Reactive] public bool? ParallelQuery { get; set; } - [Reactive] public bool? ServeStale { get; set; } + [Reactive] public bool UseSystemHosts { get; set; } + [Reactive] public bool AddCommonHosts { get; set; } + [Reactive] public bool FakeIP { get; set; } + [Reactive] public bool BlockBindingQuery { get; set; } + [Reactive] public string DirectDNS { get; set; } + [Reactive] public string RemoteDNS { get; set; } + [Reactive] public string BootstrapDNS { get; set; } + [Reactive] public string Strategy4Freedom { get; set; } + [Reactive] public string Strategy4Proxy { get; set; } + [Reactive] public string Strategy4ProxyDial { get; set; } + [Reactive] public string Hosts { get; set; } + [Reactive] public string DirectExpectedIPs { get; set; } + [Reactive] public bool ParallelQuery { get; set; } + [Reactive] public bool ServeStale { get; set; } + [Reactive] public bool EnableHappyEyeballs { get; set; } [Reactive] public bool UseSystemHostsCompatible { get; set; } [Reactive] public string DomainStrategy4FreedomCompatible { get; set; } = string.Empty; @@ -68,21 +69,21 @@ public class DNSSettingViewModel : MyReactiveObject, ICloseable { _config = AppManager.Instance.Config; var item = _config.SimpleDNSItem; - UseSystemHosts = item.UseSystemHosts; - AddCommonHosts = item.AddCommonHosts; - FakeIP = item.FakeIP; - BlockBindingQuery = item.BlockBindingQuery; - DirectDNS = item.DirectDNS; - RemoteDNS = item.RemoteDNS; - BootstrapDNS = item.BootstrapDNS; - Strategy4Freedom = item.Strategy4Freedom; - Strategy4Proxy = item.Strategy4Proxy; - Strategy4ProxyDial = item.Strategy4ProxyDial; - Hosts = item.Hosts; - DirectExpectedIPs = item.DirectExpectedIPs; - ParallelQuery = item.ParallelQuery; - ServeStale = item.ServeStale; - + UseSystemHosts = item.UseSystemHosts ?? false; + AddCommonHosts = item.AddCommonHosts ?? false; + FakeIP = item.FakeIP ?? false; + BlockBindingQuery = item.BlockBindingQuery ?? false; + DirectDNS = item.DirectDNS ?? string.Empty; + RemoteDNS = item.RemoteDNS ?? string.Empty; + BootstrapDNS = item.BootstrapDNS ?? string.Empty; + Strategy4Freedom = item.Strategy4Freedom ?? string.Empty; + Strategy4Proxy = item.Strategy4Proxy ?? string.Empty; + Strategy4ProxyDial = item.Strategy4ProxyDial ?? string.Empty; + Hosts = item.Hosts ?? string.Empty; + DirectExpectedIPs = item.DirectExpectedIPs ?? string.Empty; + ParallelQuery = item.ParallelQuery ?? false; + ServeStale = item.ServeStale ?? false; + EnableHappyEyeballs = item.EnableHappyEyeballs ?? false; var item1 = await AppManager.Instance.GetDNSItem(ECoreType.Xray); RayCustomDNSEnableCompatible = item1.Enabled; UseSystemHostsCompatible = item1.UseSystemHosts; @@ -115,7 +116,7 @@ public class DNSSettingViewModel : MyReactiveObject, ICloseable _config.SimpleDNSItem.DirectExpectedIPs = DirectExpectedIPs; _config.SimpleDNSItem.ParallelQuery = ParallelQuery; _config.SimpleDNSItem.ServeStale = ServeStale; - + _config.SimpleDNSItem.EnableHappyEyeballs = EnableHappyEyeballs; if (NormalDNSCompatible.IsNotEmpty()) { var obj = JsonUtils.ParseJson(NormalDNSCompatible); diff --git a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml index 0e9b9c4b..11fba165 100644 --- a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml @@ -42,7 +42,7 @@ x:Name="gridBasicDNSSettings" Margin="{StaticResource Margin8}" ColumnDefinitions="Auto,Auto,*" - RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> + RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> + + + + @@ -340,8 +361,8 @@ VerticalAlignment="Stretch" BorderThickness="1" Classes="TextArea" - TextWrapping="Wrap" - PlaceholderText="{x:Static resx:ResUI.TbDNSHostsConfig}" /> + PlaceholderText="{x:Static resx:ResUI.TbDNSHostsConfig}" + TextWrapping="Wrap" /> diff --git a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs index ec798920..02a5cc19 100644 --- a/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/DNSSettingWindow.axaml.cs @@ -43,6 +43,7 @@ public partial class DNSSettingWindow : WindowBase this.Bind(ViewModel, vm => vm.DirectExpectedIPs, v => v.cmbDirectExpectedIPs.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.ParallelQuery, v => v.togParallelQuery.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.ServeStale, v => v.togServeStale.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.EnableHappyEyeballs, v => v.togEnableHappyEyeballs.IsChecked).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); diff --git a/v2rayN/v2rayN/Views/DNSSettingWindow.xaml b/v2rayN/v2rayN/Views/DNSSettingWindow.xaml index 16769b02..716a00f6 100644 --- a/v2rayN/v2rayN/Views/DNSSettingWindow.xaml +++ b/v2rayN/v2rayN/Views/DNSSettingWindow.xaml @@ -53,6 +53,7 @@ + @@ -241,6 +242,28 @@ Grid.Column="1" Margin="{StaticResource Margin8}" HorizontalAlignment="Left" /> + + + + diff --git a/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs b/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs index 37c7376a..1495be77 100644 --- a/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs +++ b/v2rayN/v2rayN/Views/DNSSettingWindow.xaml.cs @@ -39,6 +39,7 @@ public partial class DNSSettingWindow this.Bind(ViewModel, vm => vm.DirectExpectedIPs, v => v.cmbDirectExpectedIPs.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.ParallelQuery, v => v.togParallelQuery.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.ServeStale, v => v.togServeStale.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.EnableHappyEyeballs, v => v.togEnableHappyEyeballs.IsChecked).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);