From 72dc25bc11bb3e9c44dc1641c745c0fdf5fb67a1 Mon Sep 17 00:00:00 2001 From: ilsha-256 Date: Thu, 25 Jun 2026 17:09:10 +0300 Subject: [PATCH] Prevent Hysteria inbounds without TLS --- app/core/xray.py | 14 +++++++- .../core-editor/kit/inbound-form-options.ts | 5 ++- tests/api/test_core.py | 35 +++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/app/core/xray.py b/app/core/xray.py index ca1986e8a..f4a8bb6ee 100644 --- a/app/core/xray.py +++ b/app/core/xray.py @@ -412,6 +412,15 @@ def _resolve_inbounds(self): self._read_inbound(inbound) self._protocols = _protocols_from_inbounds_by_tag(self._inbounds_by_tag) + @staticmethod + def _validate_hysteria_tls(protocol, stream, tag): + if protocol != "hysteria": + return + if not isinstance(stream, dict) or not stream: + raise ValueError(f"{tag} hysteria inbound requires TLS") + if stream.get("security") != "tls" or not isinstance(stream.get("tlsSettings"), dict): + raise ValueError(f"{tag} hysteria inbound requires TLS") + def _read_inbound(self, inbound: dict): """Read an inbound and its settings.""" if inbound["protocol"] not in ("vmess", "vless", "trojan", "shadowsocks", "hysteria"): @@ -439,7 +448,10 @@ def _read_inbound(self, inbound: dict): if inbound["protocol"] == "shadowsocks": self._handle_shadowsocks_settings(inbound["settings"], settings) - if stream := inbound.get("streamSettings"): + stream = inbound.get("streamSettings") + self._validate_hysteria_tls(inbound["protocol"], stream, inbound["tag"]) + + if stream: net = stream.get("network", "tcp") net_settings = stream.get(f"{net}Settings", {}) security = stream.get("security") diff --git a/dashboard/src/features/core-editor/kit/inbound-form-options.ts b/dashboard/src/features/core-editor/kit/inbound-form-options.ts index ab2ce9c40..ec0aafbeb 100644 --- a/dashboard/src/features/core-editor/kit/inbound-form-options.ts +++ b/dashboard/src/features/core-editor/kit/inbound-form-options.ts @@ -72,7 +72,10 @@ export function getInboundTransportSelectOptions( export function getInboundSecuritySelectOptions(caps: InboundFormCapabilities, protocol: Inbound['protocol']): Array<'none' | 'tls' | 'reality'> { const all = Object.keys(caps.securities).filter((k): k is 'none' | 'tls' | 'reality' => caps.securities[k]) - if (protocol === 'vmess' || protocol === 'shadowsocks' || protocol === 'hysteria') { + if (protocol === 'hysteria') { + return all.filter(s => s === 'tls') + } + if (protocol === 'vmess' || protocol === 'shadowsocks') { return all.filter(s => s !== 'reality') } return all diff --git a/tests/api/test_core.py b/tests/api/test_core.py index 5078957b2..267e3bc90 100644 --- a/tests/api/test_core.py +++ b/tests/api/test_core.py @@ -1,3 +1,5 @@ +from copy import deepcopy + from fastapi import status from app.core.xray import XRayConfig @@ -383,3 +385,36 @@ def test_get_cores_simple_search_and_sort(access_token): finally: for core_id in created_core_ids: delete_core(access_token, core_id) + + +def test_hysteria_inbound_requires_tls(access_token): + config = deepcopy(xray_config) + config["inbounds"] = [ + { + "tag": "hy2-no-tls", + "listen": "0.0.0.0", + "port": 1081, + "protocol": "hysteria", + "settings": {"version": 2, "clients": []}, + "streamSettings": { + "network": "hysteria", + "hysteriaSettings": {"version": 2}, + "security": "none", + }, + } + ] + + response = client.post( + "/api/core", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "config": config, + "name": unique_name("hysteria_no_tls"), + "type": "xray", + "exclude_inbound_tags": [], + "fallbacks_inbound_tags": [], + }, + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert "hysteria inbound requires TLS" in response.json()["detail"]