fix(hysteria2): emit https server_url for sing-box realm outbound (#9642)

sing-box requires realm.server_url as a full URL (https://host:port).
Bare host:port caused "missing host in realm server_url" on import.
Also forward stun_servers and clear ResolveRealm success message.

Fixes #9635
This commit is contained in:
dr mike
2026-06-28 06:35:41 +03:30
committed by GitHub
parent 5a9a9b7ab1
commit bf376a8fac
6 changed files with 78 additions and 1 deletions
@@ -1,8 +1,10 @@
using AwesomeAssertions; using AwesomeAssertions;
using ServiceLib.Common; using ServiceLib.Common;
using ServiceLib.Enums; using ServiceLib.Enums;
using ServiceLib.Handler.Fmt;
using ServiceLib.Manager; using ServiceLib.Manager;
using ServiceLib.Models; using ServiceLib.Models;
using ServiceLib.Models.Dto;
using ServiceLib.Services.CoreConfig; using ServiceLib.Services.CoreConfig;
using Xunit; using Xunit;
@@ -557,4 +559,37 @@ public class CoreConfigSingboxServiceTests
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global)); cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Global));
cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct)); cfg.dns.rules.Should().Contain(r => r.clash_mode == nameof(ERuleMode.Direct));
} }
[Fact]
public void GenerateClientConfigContent_Hysteria2Realm_ShouldEmitHttpsServerUrl()
{
var shareLink =
"hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
var node = Hysteria2Fmt.ResolveRealm(shareLink, out _);
node.Should().NotBeNull();
node!.CoreType = ECoreType.sing_box;
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.CoreTypeItem =
[
new CoreTypeItem { ConfigType = EConfigType.Hysteria2, CoreType = ECoreType.sing_box }
];
CoreConfigTestFactory.BindAppManagerConfig(config);
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box);
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var proxy = cfg.outbounds.First(o => o.tag == Global.ProxyTag);
proxy.type.Should().Be("hysteria2");
proxy.realm.Should().NotBeNull();
proxy.realm!.server_url.Should().StartWith("https://");
proxy.realm.server_url.Should().Contain("realm.hy2.io");
proxy.realm.token.Should().Be("public");
proxy.realm.realm_id.Should().Be("my-realm-id");
proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
proxy.server.Should().BeNull();
}
} }
@@ -1,4 +1,6 @@
using AwesomeAssertions; using AwesomeAssertions;
using ServiceLib.Handler.Fmt;
using ServiceLib.Models.Dto;
using Xunit; using Xunit;
namespace ServiceLib.Tests.Fmt; namespace ServiceLib.Tests.Fmt;
@@ -58,4 +60,32 @@ public class HyRealmTests
uri.Should().Contain("hysteria2+realm://mytoken@rendezvous.example.com"); uri.Should().Contain("hysteria2+realm://mytoken@rendezvous.example.com");
uri.Should().EndWith("#remark"); uri.Should().EndWith("#remark");
} }
[Fact]
public void ToServerUrl_ShouldIncludeSchemeForSingbox()
{
var realm = new HyRealm(
IsHttp: false,
Token: "public",
RendezvousHost: "realm.hy2.io",
RendezvousPort: 443,
RealmName: "my-realm-id",
StunList: ["turn.cloudflare.com:3478"]
);
realm.ToServerUrl().Should().Be("https://realm.hy2.io:443");
}
[Fact]
public void ResolveRealm_Issue9635_ShouldProduceHttpsServerUrl()
{
var str = "hysteria2+realm://public@realm.hy2.io/my-realm-id?auth=uuid&stun=turn.cloudflare.com%3A3478&sni=cloudflare.com&pinSHA256=xxx#Realm-Test";
var resolved = Hysteria2Fmt.ResolveRealm(str, out _);
resolved.Should().NotBeNull();
HyRealm.TryParse(resolved!.GetProtocolExtra().Hy2RealmUrl, out var realm).Should().BeTrue();
realm!.ToServerUrl().Should().StartWith("https://");
realm.ToServerUrl().Should().Contain("realm.hy2.io");
realm.StunList.Should().Contain("turn.cloudflare.com:3478");
}
} }
@@ -110,6 +110,7 @@ public class Hysteria2Fmt : BaseFmt
Hy2RealmUrl = realm.ToUri(), Hy2RealmUrl = realm.ToUri(),
}); });
msg = string.Empty;
return item; return item;
} }
@@ -257,6 +257,7 @@ public class HyRealm4Sbox
public string? server_url { get; set; } public string? server_url { get; set; }
public string? token { get; set; } public string? token { get; set; }
public string? realm_id { get; set; } public string? realm_id { get; set; }
public List<string>? stun_servers { get; set; }
} }
public class Server4Sbox : BaseServer4Sbox public class Server4Sbox : BaseServer4Sbox
+9
View File
@@ -15,6 +15,15 @@ public record HyRealm(
{ {
public string RendezvousHostPort => RendezvousPort > 0 ? $"{RendezvousHost}:{RendezvousPort}" : RendezvousHost; public string RendezvousHostPort => RendezvousPort > 0 ? $"{RendezvousHost}:{RendezvousPort}" : RendezvousHost;
/// <summary>
/// sing-box realm.server_url requires a scheme (https:// or http://).
/// </summary>
public string ToServerUrl()
{
var scheme = IsHttp ? "http" : "https";
return $"{scheme}://{RendezvousHostPort}";
}
public static bool TryParse(string? str, out HyRealm? realm) public static bool TryParse(string? str, out HyRealm? realm)
{ {
realm = null; realm = null;
@@ -275,9 +275,10 @@ public partial class CoreConfigSingboxService
{ {
var realm4Sbox = new HyRealm4Sbox() var realm4Sbox = new HyRealm4Sbox()
{ {
server_url = realm.RendezvousHostPort, server_url = realm.ToServerUrl(),
token = realm.Token, token = realm.Token,
realm_id = realm.RealmName, realm_id = realm.RealmName,
stun_servers = realm.StunList?.Count > 0 ? realm.StunList : null,
}; };
outbound.realm = realm4Sbox; outbound.realm = realm4Sbox;
outbound.server = null; outbound.server = null;