feat: WS-fronted profiles to bypass TLS handshake filtering
- add _build_ws_link: rebuild loopback panel WS links into public vless://uuid@host:port?type=ws&security=none links - matcher supports per-inbound ws block (match on xray-side port) - serves SE Sweden-WS (via Caddy :80) and NL Netherlands-WS (:80)
This commit is contained in:
@@ -542,6 +542,39 @@ def _link_port(link: str) -> Optional[int]:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _build_ws_link(panel_link: str, inbound: dict) -> Optional[str]:
|
||||||
|
"""Rebuild a panel WS link (often with localhost host) into a public client link.
|
||||||
|
|
||||||
|
Panel generates e.g. vless://uuid@127.0.0.1:10085?... for loopback-bound
|
||||||
|
WS inbounds; clients need the public front instead:
|
||||||
|
vless://uuid@<ws.host>:<ws.port>?type=ws&security=none&host=<ws.host>&path=...
|
||||||
|
Triggered by the optional "ws" block in servers.conf:
|
||||||
|
{"host": "ws.example.com", "port": 80, "xray_port": 10085}.
|
||||||
|
Returns None on parse error.
|
||||||
|
"""
|
||||||
|
import urllib.parse
|
||||||
|
ws = inbound.get("ws") or {}
|
||||||
|
if not ws.get("host") or not ws.get("port"):
|
||||||
|
return panel_link
|
||||||
|
try:
|
||||||
|
parts = urllib.parse.urlparse(panel_link.split("#")[0])
|
||||||
|
userinfo = parts.username or ""
|
||||||
|
if not userinfo:
|
||||||
|
return None
|
||||||
|
query = dict(urllib.parse.parse_qsl(parts.query, keep_blank_values=True))
|
||||||
|
query["type"] = "ws"
|
||||||
|
query["security"] = "none"
|
||||||
|
query["host"] = ws["host"]
|
||||||
|
if "path" not in query:
|
||||||
|
return None
|
||||||
|
host = ws["host"]
|
||||||
|
port = int(ws["port"])
|
||||||
|
netloc = f"{userinfo}@{host}:{port}"
|
||||||
|
# hysteria-style links carry passwords; vless carries uuid only
|
||||||
|
return f"{parts.scheme}://{netloc}?{urllib.parse.urlencode(query)}"
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
def _build_hysteria_link(panel_link: str, sub_id: str, srv: dict) -> Optional[str]:
|
def _build_hysteria_link(panel_link: str, sub_id: str, srv: dict) -> Optional[str]:
|
||||||
"""Rebuild panel hysteria2://AUTH@host:port link into a working client link.
|
"""Rebuild panel hysteria2://AUTH@host:port link into a working client link.
|
||||||
|
|
||||||
@@ -1016,6 +1049,11 @@ async def get_subscription(request: Request, subscription_id: str, format: str =
|
|||||||
|
|
||||||
ib_protocol = _get_inbound_protocol(inbound)
|
ib_protocol = _get_inbound_protocol(inbound)
|
||||||
ib_port = inbound.get("port")
|
ib_port = inbound.get("port")
|
||||||
|
# WS-fronted inbounds: panel links carry the loopback xray port;
|
||||||
|
# match on it, the public link is rebuilt below.
|
||||||
|
ws_cfg = inbound.get("ws") or {}
|
||||||
|
if ws_cfg.get("xray_port"):
|
||||||
|
ib_port = ws_cfg["xray_port"]
|
||||||
matched_link = None
|
matched_link = None
|
||||||
fallback_link = None
|
fallback_link = None
|
||||||
for candidate in links:
|
for candidate in links:
|
||||||
@@ -1045,6 +1083,10 @@ async def get_subscription(request: Request, subscription_id: str, format: str =
|
|||||||
link = matched_link
|
link = matched_link
|
||||||
if ib_protocol == "hysteria":
|
if ib_protocol == "hysteria":
|
||||||
link = _build_hysteria_link(link, subscription_id, srv) or link
|
link = _build_hysteria_link(link, subscription_id, srv) or link
|
||||||
|
if ws_cfg:
|
||||||
|
link = _build_ws_link(link, inbound)
|
||||||
|
if link is None:
|
||||||
|
continue
|
||||||
clean_link = link.split('#')[0]
|
clean_link = link.split('#')[0]
|
||||||
if clean_link in seen_links:
|
if clean_link in seen_links:
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user