zapret-kvn/xray_fluent/engines/singbox/config_builder.py
loop-uh 3f7c50d5f9
All checks were successful
Windows project source guards / test (push) Successful in 1m2s
fix: retain native sing-box ownership for Hysteria v1
2026-09-05 17:57:04 +03:00

172 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from copy import deepcopy
from typing import Any
from xray_fluent.application.protocol_core import ProtocolCore, protocol_core
_SUPPORTED_NATIVE_PROTOCOLS = {"vmess", "trojan", "shadowsocks", "socks", "http"}
# Типы, которые sing-box 1.13+ принимает только в top-level массиве `endpoints[]`,
# а не в `outbounds[]`.
_SINGBOX_ENDPOINT_TYPES = {"wireguard"}
def is_singbox_endpoint_outbound(outbound: dict[str, Any] | None) -> bool:
"""True, если конфиг ноды — sing-box endpoint (например, WireGuard/AWG)."""
if not isinstance(outbound, dict):
return False
native_type = str(outbound.get("type") or "").strip().lower()
return native_type in _SINGBOX_ENDPOINT_TYPES and not outbound.get("protocol")
def is_singbox_endpoint_node(node) -> bool:
return node is not None and is_singbox_endpoint_outbound(node.outbound)
def build_singbox_outbound(node, *, tag: str = "proxy") -> dict[str, Any]:
"""Convert a stored node outbound into a native sing-box outbound."""
source = deepcopy(node.outbound or {})
protocol = str(source.get("protocol") or "").lower()
native_type = str(source.get("type") or "").lower()
if protocol_core(node) is not ProtocolCore.SINGBOX:
raise ValueError("VLESS belongs to Xray; Hysteria2 belongs to the official Hysteria core.")
if native_type and not protocol:
# Служебные ключи приложения (`_dns` и прочие с префиксом `_`) не входят
# в схему sing-box, строгий декодер их отвергает — отбрасываем.
source = {key: value for key, value in source.items() if not str(key).startswith("_")}
source["tag"] = tag
return source
if protocol not in _SUPPORTED_NATIVE_PROTOCOLS:
raise ValueError(
f"Текущий сервер нельзя конвертировать в native sing-box outbound: protocol `{protocol or 'unknown'}`"
)
outbound = _convert_outbound(source)
unsupported_transport = str(outbound.pop("_unsupported_transport", "") or "").strip()
if unsupported_transport:
raise ValueError(
f"Текущий сервер нельзя конвертировать в native sing-box outbound: transport `{unsupported_transport}` не поддерживается"
)
outbound["tag"] = tag
return outbound
def _convert_outbound(xray_ob: dict[str, Any]) -> dict[str, Any]:
protocol = str(xray_ob.get("protocol") or "").lower()
xray_settings = dict(xray_ob.get("settings") or {})
stream = dict(xray_ob.get("streamSettings") or {})
sb: dict[str, Any] = {"type": protocol}
if protocol == "vmess":
vnext = (xray_settings.get("vnext") or [{}])[0]
sb["server"] = str(vnext.get("address") or "")
sb["server_port"] = int(vnext.get("port") or 0)
users = (vnext.get("users") or [{}])[0]
sb["uuid"] = str(users.get("id") or "")
sb["alter_id"] = int(users.get("alterId") or 0)
sb["security"] = str(users.get("security") or "auto")
elif protocol == "trojan":
servers = (xray_settings.get("servers") or [{}])[0]
sb["server"] = str(servers.get("address") or "")
sb["server_port"] = int(servers.get("port") or 0)
sb["password"] = str(servers.get("password") or "")
elif protocol == "shadowsocks":
servers = (xray_settings.get("servers") or [{}])[0]
sb["server"] = str(servers.get("address") or "")
sb["server_port"] = int(servers.get("port") or 0)
sb["method"] = str(servers.get("method") or "")
sb["password"] = str(servers.get("password") or "")
elif protocol in ("socks", "http"):
servers = (xray_settings.get("servers") or [{}])[0]
sb["server"] = str(servers.get("address") or "")
sb["server_port"] = int(servers.get("port") or 0)
user_list = servers.get("users") or []
if user_list:
sb["username"] = str(user_list[0].get("user") or "")
sb["password"] = str(user_list[0].get("pass") or "")
_apply_tls(sb, stream)
_apply_transport(sb, stream)
return sb
def _apply_tls(sb: dict[str, Any], stream: dict[str, Any]) -> None:
security = str(stream.get("security") or "").lower()
if security not in ("tls", "reality"):
return
tls: dict[str, Any] = {"enabled": True}
if security == "reality":
reality_settings = dict(stream.get("realitySettings") or {})
tls["server_name"] = str(reality_settings.get("serverName") or "")
fingerprint = str(reality_settings.get("fingerprint") or "")
if fingerprint:
tls["utls"] = {"enabled": True, "fingerprint": fingerprint}
public_key = str(reality_settings.get("publicKey") or "")
short_id = str(reality_settings.get("shortId") or "")
tls["reality"] = {"enabled": True, "public_key": public_key, "short_id": short_id}
else:
tls_settings = dict(stream.get("tlsSettings") or {})
server_name = str(tls_settings.get("serverName") or "")
if server_name:
tls["server_name"] = server_name
alpn = tls_settings.get("alpn")
if alpn:
tls["alpn"] = list(alpn)
fingerprint = str(tls_settings.get("fingerprint") or "")
if fingerprint:
tls["utls"] = {"enabled": True, "fingerprint": fingerprint}
if tls_settings.get("allowInsecure", False):
tls["insecure"] = True
sb["tls"] = tls
def _apply_transport(sb: dict[str, Any], stream: dict[str, Any]) -> None:
network = str(stream.get("network") or "tcp").lower()
if network == "tcp":
return
if network == "ws":
ws_settings = dict(stream.get("wsSettings") or {})
transport: dict[str, Any] = {"type": "ws"}
path = str(ws_settings.get("path") or "")
if path:
transport["path"] = path
headers = dict(ws_settings.get("headers") or {})
if headers:
transport["headers"] = headers
sb["transport"] = transport
return
if network in ("http", "h2"):
http_settings = dict(stream.get("httpSettings") or stream.get("h2Settings") or {})
transport = {"type": "http"}
host = http_settings.get("host")
if host:
transport["host"] = list(host) if isinstance(host, list) else [str(host)]
path = str(http_settings.get("path") or "")
if path:
transport["path"] = path
sb["transport"] = transport
return
if network == "grpc":
grpc_settings = dict(stream.get("grpcSettings") or {})
transport = {"type": "grpc"}
service_name = str(grpc_settings.get("serviceName") or "")
if service_name:
transport["service_name"] = service_name
sb["transport"] = transport
return
if network == "xhttp":
sb["_unsupported_transport"] = "xhttp"