From 029ea7031cc520a04f6b44be7616d641cdce544f Mon Sep 17 00:00:00 2001 From: SashegDev Date: Wed, 30 Sep 2026 04:37:48 +0000 Subject: [PATCH] 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) --- aggregator.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/aggregator.py b/aggregator.py index 18fe12c..b28c62b 100644 --- a/aggregator.py +++ b/aggregator.py @@ -542,6 +542,39 @@ def _link_port(link: str) -> Optional[int]: except Exception: 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@:?type=ws&security=none&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]: """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_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 fallback_link = None for candidate in links: @@ -1045,6 +1083,10 @@ async def get_subscription(request: Request, subscription_id: str, format: str = link = matched_link if ib_protocol == "hysteria": 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] if clean_link in seen_links: continue