From f332caf5076353a7c764efcb6e8e0eb1b7c4249a Mon Sep 17 00:00:00 2001 From: Miheichev Aleksandr Sergeevich Date: Fri, 28 Aug 2026 06:33:05 +0000 Subject: [PATCH] fix(hysteria2): default the port to 443 when the share URI omits it (#10026) The Hysteria2 URI scheme makes the port optional: "The hostname and optional port of the server. If the port is omitted, it defaults to 443." `Hysteria2Fmt.Resolve` assigned `url.Port` straight through, and `System.Uri` answers -1 for an unregistered scheme that carries no port, so `hysteria2://password@hy2.example/` imported as a profile with `Port = -1`. `ProfileItem.IsValid` rejects any port outside 1..65535, so such a link produced a profile that could never be used, and nothing said why. -1 is the only value that means "the port was omitted"; a ':' with no digits after it maps to -1 as well. An explicit ":0" parses as 0 and keeps the fate it has today - rejected by `IsValid` - rather than being redirected to a server the link never named. `ResolveRealm` takes its port from `HyRealm.RendezvousPort` instead of the URI, so it is unaffected. The added tests cover both spellings of the scheme, with and without a trailing slash, a bare ':', and the resulting profile's validity. Two of them are controls: an explicit port is still preserved, and an explicit ":0" still does not turn into 443. --- .../ServiceLib.Tests/Fmt/Hysteria2FmtTests.cs | 54 +++++++++++++++++++ v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs | 6 ++- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 v2rayN/ServiceLib.Tests/Fmt/Hysteria2FmtTests.cs diff --git a/v2rayN/ServiceLib.Tests/Fmt/Hysteria2FmtTests.cs b/v2rayN/ServiceLib.Tests/Fmt/Hysteria2FmtTests.cs new file mode 100644 index 00000000..f085d220 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/Fmt/Hysteria2FmtTests.cs @@ -0,0 +1,54 @@ +namespace ServiceLib.Tests.Fmt; + +public class Hysteria2FmtTests +{ + // "The hostname and optional port of the server. If the port is omitted, it defaults to 443." + // -- https://v2.hysteria.network/docs/developers/URI-Scheme/ + // A ':' with no digits after it is an omitted port too, per RFC 3986 'port = *DIGIT'. + [Test] + [Arguments("hysteria2://password@hy2.example/")] + [Arguments("hysteria2://password@hy2.example")] + [Arguments("hysteria2://password@hy2.example:/")] + [Arguments("hy2://password@hy2.example/?sni=real.example")] + public async Task ResolveConfig_WithoutPort_ShouldDefaultTo443(string shareUri) + { + var resolved = FmtHandler.ResolveConfig(shareUri, out var msg); + + await resolved.Should().NotBeNull().Because($"uri: {shareUri}, msg: {msg}"); + await resolved!.ConfigType.Should().BeEqualTo(EConfigType.Hysteria2); + await resolved.Address.Should().BeEqualTo("hy2.example"); + await resolved.Port.Should().BeEqualTo(443); + } + + [Test] + public async Task ResolveConfig_WithoutPort_ShouldProduceAValidProfile() + { + // Uri.Port is -1 for an unregistered scheme with no port, and ProfileItem.IsValid rejects + // any port outside 1..65535 - so the default is what keeps such a link usable at all. + var resolved = FmtHandler.ResolveConfig("hysteria2://password@hy2.example/", out _); + + await resolved.Should().NotBeNull(); + await resolved!.IsValid().Should().BeTrue(); + } + + [Test] + public async Task ResolveConfig_WithExplicitPort_ShouldKeepIt() + { + var resolved = FmtHandler.ResolveConfig("hysteria2://password@hy2.example:8443/", out _); + + await resolved.Should().NotBeNull(); + await resolved!.Port.Should().BeEqualTo(8443); + } + + [Test] + public async Task ResolveConfig_WithExplicitZeroPort_ShouldNotApplyTheDefault() + { + // Uri.Port is 0 here, not -1: the port is present, it is just not a usable one. Treating + // it as "omitted" would silently move the endpoint to :443, so it stays invalid instead. + var resolved = FmtHandler.ResolveConfig("hysteria2://password@hy2.example:0/", out _); + + await resolved.Should().NotBeNull(); + await resolved!.Port.Should().BeEqualTo(0); + await resolved.IsValid().Should().BeFalse(); + } +} diff --git a/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs b/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs index 7b7153e4..89af4e78 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/Hysteria2Fmt.cs @@ -17,7 +17,11 @@ public class Hysteria2Fmt : BaseFmt } item.Address = url.IdnHost; - item.Port = url.Port; + // The URI scheme makes the port optional and defaults it to 443. Uri.Port answers -1 for + // an unregistered scheme carrying no port, which ProfileItem.IsValid then rejects. + // Only -1 means "omitted": an explicit ":0" has to stay 0 and be rejected the way it + // always was, instead of being quietly redirected to a server the link never named. + item.Port = url.Port == -1 ? 443 : url.Port; item.Remarks = url.GetComponents(UriComponents.Fragment, UriFormat.Unescaped); item.Password = Utils.UrlDecode(url.UserInfo);