From 0734317f9f22a244c9d63ed08e56559b3962df36 Mon Sep 17 00:00:00 2001 From: shizukunacho <101909939+shizukunacho@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:31:53 +0800 Subject: [PATCH] fix(singbox): preserve Hysteria2 hop interval ranges (#10576) * fix(singbox): use both bounds of Hysteria2 hop interval * fix(singbox): preserve native Hysteria2 hop interval bounds --- .../Singbox/CoreConfigSingboxServiceTests.cs | 26 +++++++++++++++++++ .../Models/CoreConfigs/SingboxConfig.cs | 1 + .../Singbox/SingboxOutboundService.cs | 7 +++-- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs b/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs index eb2c2404..2eafa963 100644 --- a/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs +++ b/v2rayN/ServiceLib.Tests/CoreConfig/Singbox/CoreConfigSingboxServiceTests.cs @@ -715,6 +715,32 @@ public class CoreConfigSingboxServiceTests } } + [Test] + [Arguments("5-15", "5s", "15s")] + [Arguments("20-25", "20s", "25s")] + [Arguments("15", "15s", null)] + [Arguments("4-10", "30s", null)] + [Arguments("15-5", "30s", null)] + public async Task GenerateClientConfigContent_Hysteria2HopInterval_ShouldPreserveRange(string interval, string expected, string? expectedMax) + { + var node = Hysteria2Fmt.Resolve("hysteria2://password@example.com:443?mport=20000-50000", out _)!; + node.CoreType = ECoreType.sing_box; + node.SetProtocolExtra(node.GetProtocolExtra() with { HopInterval = interval }); + + var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box); + config.HysteriaItem.HopInterval = 30; + CoreConfigTestFactory.BindAppManagerConfig(config); + var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box); + + var result = new CoreConfigSingboxService(context).GenerateClientConfigContent(); + + await result.Success.Should().BeTrue().Because($"ret msg: {result.Msg}"); + var cfg = JsonUtils.Deserialize(result.Data!.ToString())!; + var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag); + await proxy.hop_interval.Should().BeEqualTo(expected); + await proxy.hop_interval_max.Should().BeEqualTo(expectedMax); + } + [Test] public async Task GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl() { diff --git a/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs b/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs index c62cc09f..5bbd7b90 100644 --- a/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigs/SingboxConfig.cs @@ -133,6 +133,7 @@ public class Outbound4Sbox : BaseServer4Sbox public int? alter_id { get; set; } public string? flow { get; set; } public string? hop_interval { get; set; } + public string? hop_interval_max { get; set; } public int? up_mbps { get; set; } public int? down_mbps { get; set; } public string? auth_str { get; set; } diff --git a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs index 7cbea305..455e0e18 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs @@ -277,13 +277,12 @@ public partial class CoreConfigSingboxService } else if (protocolExtra.HopInterval?.Contains('-') ?? false) { - // may be a range like 5-10 var parts = protocolExtra.HopInterval.Split('-'); if (parts.Length == 2 && int.TryParse(parts[0], out var hiL) && - int.TryParse(parts[0], out var hiH)) + int.TryParse(parts[1], out var hiH) && hiL >= 5 && hiH >= hiL) { - var hi = (hiL + hiH) / 2; - outbound.hop_interval = hi >= 5 ? $"{hi}s" : outbound.hop_interval; + outbound.hop_interval = $"{hiL}s"; + outbound.hop_interval_max = $"{hiH}s"; } } }