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:
1 parent
a074568cd9
commit
b492f8f4eb
3 files changed
+88
-4
No files matched your search
@@ -442,11 +442,12 @@ def parse_vless(url):
|
|||||||
parsed = urlsplit(url)
|
parsed = urlsplit(url)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ProxyError("неверная VLESS-ссылка")
|
raise ProxyError("неверная VLESS-ссылка")
|
||||||
|
# Новые ссылки Hiddify пишут «/» перед «?»: ...@SERVER:PORT/?type=...
|
||||||
if (
|
if (
|
||||||
parsed.scheme != "vless"
|
parsed.scheme != "vless"
|
||||||
or not parsed.netloc
|
or not parsed.netloc
|
||||||
or parsed.password is not None
|
or parsed.password is not None
|
||||||
or parsed.path
|
or parsed.path not in ("", "/")
|
||||||
):
|
):
|
||||||
raise ProxyError("ожидалась одна ссылка vless://UUID@SERVER:PORT")
|
raise ProxyError("ожидалась одна ссылка vless://UUID@SERVER:PORT")
|
||||||
try:
|
try:
|
||||||
@@ -464,8 +465,11 @@ def parse_vless(url):
|
|||||||
validate_address(server)
|
validate_address(server)
|
||||||
query = parse_query(parsed.query)
|
query = parse_query(parsed.query)
|
||||||
required(query, "type", "security", "encryption", "sni", "fp")
|
required(query, "type", "security", "encryption", "sni", "fp")
|
||||||
if query["encryption"] != "none":
|
# Кроме none, Xray принимает VLESS-шифрование вида
|
||||||
raise ProxyError("поддерживается только encryption=none")
|
# mlkem768x25519plus.РЕЖИМ.RTT.КЛЮЧ...; подробности проверит xray run -test.
|
||||||
|
encrypted = query["encryption"] != "none"
|
||||||
|
if encrypted and not query["encryption"].startswith("mlkem768x25519plus."):
|
||||||
|
raise ProxyError("поддерживается только encryption=none или mlkem768x25519plus")
|
||||||
if query.get("flow") not in (None, "xtls-rprx-vision"):
|
if query.get("flow") not in (None, "xtls-rprx-vision"):
|
||||||
raise ProxyError("поддерживается только flow=xtls-rprx-vision")
|
raise ProxyError("поддерживается только flow=xtls-rprx-vision")
|
||||||
if query.get("headerType", "none") != "none":
|
if query.get("headerType", "none") != "none":
|
||||||
@@ -485,7 +489,8 @@ def parse_vless(url):
|
|||||||
raise ProxyError("параметры профиля несовместимы с TCP/REALITY")
|
raise ProxyError("параметры профиля несовместимы с TCP/REALITY")
|
||||||
elif query["type"] == "xhttp" and query["security"] == "tls":
|
elif query["type"] == "xhttp" and query["security"] == "tls":
|
||||||
required(query, "host", "path")
|
required(query, "host", "path")
|
||||||
if "flow" in query or "pbk" in query or "sid" in query:
|
# Vision поверх XHTTP работает только внутри VLESS-шифрования.
|
||||||
|
if ("flow" in query and not encrypted) or "pbk" in query or "sid" in query:
|
||||||
raise ProxyError("параметры профиля несовместимы с XHTTP/TLS")
|
raise ProxyError("параметры профиля несовместимы с XHTTP/TLS")
|
||||||
if "pcs" in query:
|
if "pcs" in query:
|
||||||
hashes = list_value(query["pcs"])
|
hashes = list_value(query["pcs"])
|
||||||
@@ -1188,6 +1193,8 @@ def safe_summary(profile, local_port):
|
|||||||
),
|
),
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
|
if profile.query["encryption"] != "none":
|
||||||
|
print("VLESS-шифрование: mlkem768x25519plus", flush=True)
|
||||||
print("UUID и ключи: скрыты", flush=True)
|
print("UUID и ключи: скрыты", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -345,6 +345,12 @@ source ~/.config/proxy-client/shell.bash
|
|||||||
повторные и несовместимые query-параметры отклоняются. XHTTP поддерживает ALPN
|
повторные и несовместимые query-параметры отклоняются. XHTTP поддерживает ALPN
|
||||||
`http/1.1`, `h2` и `h3`; `extra` должен быть JSON-объектом без повторных ключей.
|
`http/1.1`, `h2` и `h3`; `extra` должен быть JSON-объектом без повторных ключей.
|
||||||
|
|
||||||
|
Кроме `encryption=none`, принимается VLESS-шифрование
|
||||||
|
`encryption=mlkem768x25519plus.…` из новых ссылок Hiddify; остальную часть
|
||||||
|
значения проверяет сам Xray. Только с таким шифрованием XHTTP допускает
|
||||||
|
`flow=xtls-rprx-vision`. Косая черта перед параметрами (`…:443/?type=…`)
|
||||||
|
допустима, другой путь в ссылке — нет.
|
||||||
|
|
||||||
Xray 26.3.27 использует `pcs` и `vcn`, а устаревшие `allowInsecure` и
|
Xray 26.3.27 использует `pcs` и `vcn`, а устаревшие `allowInsecure` и
|
||||||
`insecure` не переносит. Истинное legacy-значение даёт предупреждение, но
|
`insecure` не переносит. Истинное legacy-значение даёт предупреждение, но
|
||||||
обычная проверка сертификата остаётся включённой.
|
обычная проверка сертификата остаётся включённой.
|
||||||
|
|||||||
@@ -1231,5 +1231,76 @@ class XrayInstallTest(unittest.TestCase):
|
|||||||
self.assertIn("уже установлен", result.stdout)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in new issue
Block a user