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.
This commit is contained in:
SashegDev
2026-09-30 04:01:20 +00:00
parent 48abeb04d5
commit 65cfec7137
+37 -5
View File
@@ -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