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