From 65cfec71375c2ed7dc68a733726d86e5aa97f88f Mon Sep 17 00:00:00 2001 From: SashegDev Date: Wed, 30 Sep 2026 04:01:20 +0000 Subject: [PATCH] fix: deterministic inbound link matching by port Panel duplicate client entries across inbounds made the first-unseen matcher serve wrong links (e.g. Moscow Direct got :46457 instead of :37744), nondeterministically per cache rebuild. Match candidates by inbound port from servers.conf, falling back to legacy behavior when no port matches. --- aggregator.py | 42 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/aggregator.py b/aggregator.py index 3c0b52d..18fe12c 100644 --- a/aggregator.py +++ b/aggregator.py @@ -526,6 +526,22 @@ def _link_protocol(link: str) -> str: def _get_inbound_protocol(inbound: dict) -> str: return inbound.get("protocol", "vless") +def _link_port(link: str) -> Optional[int]: + """Extract port number from a proxy link URI. Returns None if unparseable.""" + try: + body = link.split('#')[0] + # scheme://[userinfo@]host:port[?...] + after_scheme = body.split("://", 1)[1] + hostport = after_scheme.split("@")[-1].split("?")[0].split("/")[0] + if ":" not in hostport: + return None + port_str = hostport.rsplit(":", 1)[1] + # strip IPv6 brackets edge: [::1]:port + port_str = port_str.rstrip("]") + return int(port_str) + 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. @@ -999,13 +1015,29 @@ async def get_subscription(request: Request, subscription_id: str, format: str = continue ib_protocol = _get_inbound_protocol(inbound) + ib_port = inbound.get("port") matched_link = None + fallback_link = None for candidate in links: - if _link_protocol(candidate) == ib_protocol: - clean_candidate = candidate.split('#')[0] - if clean_candidate not in seen_links: - matched_link = candidate - break + if _link_protocol(candidate) != ib_protocol: + continue + clean_candidate = candidate.split('#')[0] + if clean_candidate in seen_links: + continue + # Prefer the link whose port matches this inbound's port. + # Duplicate client entries across inbounds otherwise make + # the match nondeterministic (wrong port served). + if ib_port is not None and _link_port(candidate) == ib_port: + matched_link = candidate + break + if fallback_link is None: + fallback_link = candidate + + if matched_link is None: + # No port match (e.g. panel changed ports and servers.conf + # is stale): fall back to legacy first-unseen behavior so + # the profile is still served instead of dropped. + matched_link = fallback_link if not matched_link: continue