From 27b8f889fe5e90d5f12c33b0cd6209e064b0ec98 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Fri, 5 Jun 2026 11:45:49 +0000 Subject: [PATCH] Tail Fragmentation (#9476) --- .../ServiceLib/Models/Configs/ConfigItems.cs | 2 + .../Models/CoreConfigs/SingboxConfig.cs | 1 + .../Models/CoreConfigs/V2rayConfig.cs | 1 + v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 18 ++++ v2rayN/ServiceLib/Resx/ResUI.resx | 6 ++ v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 6 ++ .../Singbox/SingboxRoutingService.cs | 17 ++++ .../V2ray/CoreConfigV2rayService.cs | 12 +++ .../CoreConfig/V2ray/V2rayOutboundService.cs | 87 ++++++++++++++----- .../ViewModels/OptionSettingViewModel.cs | 3 + .../Views/OptionSettingWindow.axaml | 38 ++++++-- .../Views/OptionSettingWindow.axaml.cs | 1 + v2rayN/v2rayN/Views/OptionSettingWindow.xaml | 39 +++++++-- .../v2rayN/Views/OptionSettingWindow.xaml.cs | 1 + 14 files changed, 192 insertions(+), 40 deletions(-) diff --git a/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs b/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs index 7a0f2d87..ea2dcaac 100644 --- a/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/Configs/ConfigItems.cs @@ -17,6 +17,8 @@ public class CoreBasicItem public bool EnableFragment { get; set; } + public bool EnableFinalFragment { get; set; } + public bool EnableCacheFile4Sbox { get; set; } = true; } diff --git a/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs b/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs index a5249acb..136e8f27 100644 --- a/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs @@ -93,6 +93,7 @@ public class Rule4Sbox public List? wifi_bssid { get; set; } public bool? rule_set_ip_cidr_match_source { get; set; } public bool? rule_set_ip_cidr_accept_empty { get; set; } + public bool? tls_record_fragment { get; set; } } [Serializable] diff --git a/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs index 52cab623..cd5b68a0 100644 --- a/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/V2rayConfig.cs @@ -489,6 +489,7 @@ public class UdpHop4Ray public class Finalmask4Ray { + public List? tcp { get; set; } public List? udp { get; set; } public QuicParams4Ray? quicParams { get; set; } } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 6880de14..68bec1bb 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -3033,6 +3033,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Enable Final Fragment 的本地化字符串。 + /// + public static string TbEnableFinalFragment { + get { + return ResourceManager.GetString("TbEnableFinalFragment", resourceCulture); + } + } + + /// + /// 查找类似 Split the tail of packets into smaller fragments. This may affect throughput and latency. 的本地化字符串。 + /// + public static string TbEnableFinalFragmentTip { + get { + return ResourceManager.GetString("TbEnableFinalFragmentTip", resourceCulture); + } + } + /// /// 查找类似 Enable Tun 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index f049ef98..0b82e788 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1764,4 +1764,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Conflict between {0} and {1} + + Enable Final Fragment + + + Split the tail of packets into smaller fragments. This may affect throughput and latency. + \ 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 28337430..ada0d389 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1761,4 +1761,10 @@ {0} 与 {1} 冲突 + + 启用末端分片 + + + 将数据包末端拆分为更小片段发送,可能影响吞吐与延迟。 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs index b39312eb..2a1df21f 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxRoutingService.cs @@ -96,6 +96,15 @@ public partial class CoreConfigSingboxService new() { protocol = ["dns"] }, ], }); + if (_config.CoreBasicItem.EnableFinalFragment) + { + _coreConfig.route.rules.Add(new() + { + protocol = ["tls"], + action = "route-options", + tls_record_fragment = true, + }); + } } else { @@ -104,6 +113,14 @@ public partial class CoreConfigSingboxService port = [53], action = "hijack-dns", }); + if (_config.CoreBasicItem.EnableFinalFragment) + { + _coreConfig.route.rules.Add(new() + { + action = "route-options", + tls_record_fragment = true, + }); + } } var hostsDomains = new List(); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs index 85963bb4..9e762c05 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/CoreConfigV2rayService.cs @@ -60,6 +60,10 @@ public partial class CoreConfigV2rayService(CoreConfigContext context) { ApplyOutboundFragment(); } + if (_config.CoreBasicItem.EnableFinalFragment) + { + ApplyFinalFragment(); + } ApplyOutboundBindInterface(); ApplyOutboundSendThrough(); @@ -204,6 +208,10 @@ public partial class CoreConfigV2rayService(CoreConfigContext context) { ApplyOutboundFragment(); } + if (_config.CoreBasicItem.EnableFinalFragment) + { + ApplyFinalFragment(); + } ApplyOutboundBindInterface(); ApplyOutboundSendThrough(); //ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary()); @@ -276,6 +284,10 @@ public partial class CoreConfigV2rayService(CoreConfigContext context) { ApplyOutboundFragment(); } + if (_config.CoreBasicItem.EnableFinalFragment) + { + ApplyFinalFragment(); + } ApplyOutboundBindInterface(); ApplyOutboundSendThrough(); diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs index 458b86bb..6441dcd9 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -826,29 +826,7 @@ public partial class CoreConfigV2rayService && (n.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true)) .ToList(); - var configPackets = _config.Fragment4RayItem?.Packets ?? "tlshello"; - var configLength = _config.Fragment4RayItem?.Length ?? "50-100"; - var configDelay = _config.Fragment4RayItem?.Interval ?? "10-20"; - - var fragmentMask = new Mask4Ray - { - type = "fragment", - settings = new MaskSettings4Ray - { - packets = configPackets, - length = configLength, - delay = configDelay, - } - }; - var noiseMask = new Mask4Ray - { - type = "noise", - settings = new MaskSettings4Ray - { - length = "10-20", - delay = "10-16", - } - }; + var (fragmentMask, noiseMask) = BuildFragmentsMasks(); foreach (var outbound in actOutboundWithTlsList) { @@ -882,4 +860,67 @@ public partial class CoreConfigV2rayService outbound.streamSettings.finalmask = finalMaskJsonObj; } } + + private void ApplyFinalFragment() + { + var (fragmentMask, noiseMask) = BuildFragmentsMasks(); + var actOutboundList = _coreConfig.outbounds.Where(n => n.tag.StartsWith(Global.ProxyTag)).ToList(); + + var fragmentFreedom = new Outbounds4Ray() + { + tag = $"{Global.ProxyTag}-fragment-freedom", + protocol = "freedom", + streamSettings = new StreamSettings4Ray + { + finalmask = new Finalmask4Ray + { + tcp = [fragmentMask], + udp = [noiseMask], + } + } + }; + + foreach (var outbound in actOutboundList) + { + var index = _coreConfig.outbounds.IndexOf(outbound); + var originalTag = outbound.tag; + var afterTag = $"fragment-{originalTag}"; + var cloneFragmentFreedom = JsonUtils.DeepCopy(fragmentFreedom); + cloneFragmentFreedom.tag = originalTag; + cloneFragmentFreedom.streamSettings.sockopt ??= new(); + cloneFragmentFreedom.streamSettings.sockopt.dialerProxy = afterTag; + + outbound.tag = afterTag; + _coreConfig.outbounds.Insert(index, cloneFragmentFreedom); + } + } + + private (Mask4Ray tcpFragment, Mask4Ray udpNoise) BuildFragmentsMasks() + { + var configPackets = _config.Fragment4RayItem?.Packets ?? "tlshello"; + var configLength = _config.Fragment4RayItem?.Length ?? "50-100"; + var configDelay = _config.Fragment4RayItem?.Interval ?? "10-20"; + + var fragmentMask = new Mask4Ray + { + type = "fragment", + settings = new MaskSettings4Ray + { + packets = configPackets, + length = configLength, + delay = configDelay, + } + }; + var noiseMask = new Mask4Ray + { + type = "noise", + settings = new MaskSettings4Ray + { + length = "10-20", + delay = "10-16", + } + }; + + return (fragmentMask, noiseMask); + } } diff --git a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs index 85af3d02..39a0fcd4 100644 --- a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs @@ -25,6 +25,7 @@ public class OptionSettingViewModel : MyReactiveObject [Reactive] public int? HyUpMbps { get; set; } [Reactive] public int? HyDownMbps { get; set; } [Reactive] public bool EnableFragment { get; set; } + [Reactive] public bool EnableFinalFragment { get; set; } #endregion Core @@ -161,6 +162,7 @@ public class OptionSettingViewModel : MyReactiveObject HyUpMbps = _config.HysteriaItem.UpMbps; HyDownMbps = _config.HysteriaItem.DownMbps; EnableFragment = _config.CoreBasicItem.EnableFragment; + EnableFinalFragment = _config.CoreBasicItem.EnableFinalFragment; #endregion Core @@ -351,6 +353,7 @@ public class OptionSettingViewModel : MyReactiveObject _config.HysteriaItem.UpMbps = HyUpMbps ?? 0; _config.HysteriaItem.DownMbps = HyDownMbps ?? 0; _config.CoreBasicItem.EnableFragment = EnableFragment; + _config.CoreBasicItem.EnableFinalFragment = EnableFinalFragment; _config.GuiItem.AutoRun = AutoRun; _config.GuiItem.EnableStatistics = EnableStatistics; diff --git a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml index aa187d47..f4e8fbc8 100644 --- a/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/OptionSettingWindow.axaml @@ -38,7 +38,7 @@ + RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> - + + Margin="{StaticResource Margin4}" + HorizontalAlignment="Left" /> + + + + this.Bind(ViewModel, vm => vm.HyUpMbps, v => v.txtUpMbps.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.HyDownMbps, v => v.txtDownMbps.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.EnableFragment, v => v.togenableFragment.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.EnableFinalFragment, v => v.togenableFinalFragment.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.AutoRun, v => v.togAutoRun.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.EnableStatistics, v => v.togEnableStatistics.IsChecked).DisposeWith(disposables); diff --git a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml index e1e7e988..da42494c 100644 --- a/v2rayN/v2rayN/Views/OptionSettingWindow.xaml +++ b/v2rayN/v2rayN/Views/OptionSettingWindow.xaml @@ -66,6 +66,7 @@ + @@ -371,21 +372,20 @@ Margin="{StaticResource Margin8}" VerticalAlignment="Center" Style="{StaticResource ToolbarTextBlock}" - Text="{x:Static resx:ResUI.TbSettingsBindInterface}" /> - + + HorizontalAlignment="Left" /> + + + + vm.HyUpMbps, v => v.txtUpMbps.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.HyDownMbps, v => v.txtDownMbps.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.EnableFragment, v => v.togenableFragment.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.EnableFinalFragment, v => v.togenableFinalFragment.IsChecked).DisposeWith(disposables); //this.Bind(ViewModel, vm => vm.Kcpmtu, v => v.txtKcpmtu.Text).DisposeWith(disposables); //this.Bind(ViewModel, vm => vm.Kcptti, v => v.txtKcptti.Text).DisposeWith(disposables);