From 522bca492c0ba8cac3fadb1a009daa0790b2a202 Mon Sep 17 00:00:00 2001 From: Wei-Lun Li Date: Tue, 3 Nov 2026 17:00:48 +0900 Subject: [PATCH] fix(fmt): import legacy base64 SOCKS links again (#10505) SocksFmt.Resolve tries ResolveSocksNew first and only falls back to the legacy socks://base64(user:pass@host:port) parser when System.Uri rejects the link. System.Uri accepts most legacy links, though: it takes the base64 as the host (lower-cased) and anything after a '/' as the path. It only rejects a base64 that ends in '=' padding and has no '/', so links without padding, or with a '/' or '_' in the base64, were imported with the base64 as the address, port -1 and no credentials. A legacy link has no port of its own, so let ResolveSocksNew give up when the URI has none and leave the link to ResolveSocks. Co-authored-by: Claude Opus 5.5 --- v2rayN/ServiceLib.Tests/Fmt/SocksFmtTests.cs | 40 ++++++++++++++++++++ v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs | 5 ++- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 v2rayN/ServiceLib.Tests/Fmt/SocksFmtTests.cs diff --git a/v2rayN/ServiceLib.Tests/Fmt/SocksFmtTests.cs b/v2rayN/ServiceLib.Tests/Fmt/SocksFmtTests.cs new file mode 100644 index 00000000..33182d38 --- /dev/null +++ b/v2rayN/ServiceLib.Tests/Fmt/SocksFmtTests.cs @@ -0,0 +1,40 @@ +namespace ServiceLib.Tests.Fmt; + +public class SocksFmtTests +{ + // The legacy form base64-encodes the whole "user:pass@host:port". + [Test] + [Arguments("socks://YWRtaW46YWRtaW5AMS4yLjMuNDoxMDgw#demo", "admin", "admin", "1.2.3.4", 1080)] + [Arguments("socks://dXNlcjpwYXNzQDEuMi4zLjQ6MTA4MA==#demo", "user", "pass", "1.2.3.4", 1080)] + [Arguments("socks://dXNlcjpxd2VydHk/QDEuMi4zLjQ6MTA4MA==#demo", "user", "qwerty?", "1.2.3.4", 1080)] + [Arguments("socks://dXNlcjpxd2VydHk_QDEuMi4zLjQ6MTA4MA#demo", "user", "qwerty?", "1.2.3.4", 1080)] + public async Task ResolveConfig_LegacyBase64_ShouldDecodeServerAndCredentials( + string shareUri, string username, string password, string address, int port) + { + var resolved = FmtHandler.ResolveConfig(shareUri, out var msg); + + await resolved.Should().NotBeNull().Because($"uri: {shareUri}, msg: {msg}"); + await resolved!.ConfigType.Should().BeEqualTo(EConfigType.SOCKS); + await resolved.Address.Should().BeEqualTo(address); + await resolved.Port.Should().BeEqualTo(port); + await resolved.Username.Should().BeEqualTo(username); + await resolved.Password.Should().BeEqualTo(password); + await resolved.Remarks.Should().BeEqualTo("demo"); + } + + [Test] + [Arguments("socks://YWRtaW46YWRtaW4@1.2.3.4:1080#demo", "admin", "admin")] + [Arguments("socks://admin:admin@1.2.3.4:1080#demo", "admin", "admin")] + [Arguments("socks://1.2.3.4:1080#demo", "", "")] + public async Task ResolveConfig_HostAndPort_ShouldKeepParsingIt(string shareUri, string username, string password) + { + var resolved = FmtHandler.ResolveConfig(shareUri, out var msg); + + await resolved.Should().NotBeNull().Because($"uri: {shareUri}, msg: {msg}"); + await resolved!.Address.Should().BeEqualTo("1.2.3.4"); + await resolved.Port.Should().BeEqualTo(1080); + await resolved.Username.Should().BeEqualTo(username); + await resolved.Password.Should().BeEqualTo(password); + await resolved.Remarks.Should().BeEqualTo("demo"); + } +} diff --git a/v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs index 7edd0939..74ec6cdd 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/SocksFmt.cs @@ -86,7 +86,10 @@ public class SocksFmt : BaseFmt private static ProfileItem? ResolveSocksNew(string result) { var parsedUrl = Utils.TryUri(result); - if (parsedUrl == null) + // A legacy socks://base64(user:pass@host:port) link has no port of its own, but System.Uri + // accepts many of them, reading the base64 as the host (and whatever follows a '/' as the + // path). Leave those to ResolveSocks. + if (parsedUrl == null || parsedUrl.Port == -1) { return null; }