fix(proxy-client): поддержка новых VLESS-ссылок Hiddify

Новые ссылки Hiddify пишут «/» перед параметрами, используют
VLESS-шифрование mlkem768x25519plus и flow=xtls-rprx-vision поверх XHTTP.
Разбор принимает эти ссылки; прежние форматы работают как раньше.
Vision поверх XHTTP разрешён только вместе с шифрованием.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ddadminandClaude Opus 5.5 committed 2026-09-28 08:40:36 -04:00
1 parent a074568cd9
commit b492f8f4eb
3 files changed
+88 -4

No files matched your search

+71
View File
@@ -1231,5 +1231,76 @@ class XrayInstallTest(unittest.TestCase):
self.assertIn("уже установлен", result.stdout)
class VlessProfileTest(unittest.TestCase):
UUID = "11111111-2222-4333-8444-555555555555"
ENCRYPTION = "mlkem768x25519plus.native.0rtt.SYNTHETIC_CLIENT_KEY"
EXTRA = (
"%7B%22downloadSettings%22%3A%7B%22address%22%3A%22cdn.example.invalid%22"
"%2C%22port%22%3A443%2C%22network%22%3A%22xhttp%22%7D%7D"
)
OLD_REALITY = (
"vless://{}@example.invalid:443?type=tcp&security=reality&encryption=none"
"&flow=xtls-rprx-vision&sni=front.example.invalid&fp=chrome"
"&pbk=SYNTHETIC_PUBLIC_KEY&sid=SYNTHETIC_SHORT_ID&headerType=none#old"
).format(UUID)
OLD_XHTTP = (
"vless://{}@example.invalid:443?type=xhttp&security=tls&encryption=none"
"&sni=cdn.example.invalid&fp=chrome&host=cdn.example.invalid&path=%2Fp"
"&alpn=h2&mode=auto"
).format(UUID)
# Формат Hiddify с VLESS-шифрованием: «/» перед «?» и flow поверх XHTTP.
NEW_XHTTP = (
"vless://{}@192.0.2.1:443/?hiddify=1&fp=chrome&sni=cdn.example.invalid"
"&security=tls&alpn=h3&type=xhttp&mode=auto&path=%2Fp"
"&host=cdn.example.invalid&extra={}&flow=xtls-rprx-vision"
"&encryption={}#xhttp%20vless%20QUIC"
).format(UUID, EXTRA, ENCRYPTION)
def setUp(self):
self.module = runpy.run_path(str(CLI))
def outbound(self, url):
profile = self.module["parse_vless"](url)
config = self.module["build_xray_config"](profile, "127.0.0.1", 12334)
return config["outbounds"][0]
def test_old_formats_still_parse(self):
reality = self.outbound(self.OLD_REALITY)
self.assertEqual("none", reality["settings"]["encryption"])
self.assertEqual("reality", reality["streamSettings"]["security"])
xhttp = self.outbound(self.OLD_XHTTP)
self.assertEqual("none", xhttp["settings"]["encryption"])
self.assertNotIn("flow", xhttp["settings"])
self.assertEqual(["h2"], xhttp["streamSettings"]["tlsSettings"]["alpn"])
def test_new_hiddify_xhttp_with_vless_encryption_parses(self):
outbound = self.outbound(self.NEW_XHTTP)
settings = outbound["settings"]
stream = outbound["streamSettings"]
self.assertEqual(self.ENCRYPTION, settings["encryption"])
self.assertEqual("xtls-rprx-vision", settings["flow"])
self.assertEqual("192.0.2.1", settings["address"])
self.assertEqual(["h3"], stream["tlsSettings"]["alpn"])
self.assertEqual(
"cdn.example.invalid",
stream["xhttpSettings"]["extra"]["downloadSettings"]["address"],
)
def test_incompatible_profiles_are_still_rejected(self):
cases = {
"flow over XHTTP without encryption": self.OLD_XHTTP
+ "&flow=xtls-rprx-vision",
"unknown encryption": self.NEW_XHTTP.replace(
"mlkem768x25519plus.", "aes-128-gcm."
),
"non-root path": self.NEW_XHTTP.replace(":443/?", ":443/sub?"),
}
for label, url in cases.items():
with self.subTest(label=label):
with self.assertRaises(self.module["ProxyError"]):
self.module["parse_vless"](url)
if __name__ == "__main__":
unittest.main()