Skip to content

fix: ensure proxy stable IDs are unique for distinct configs#137

Closed
yagee wants to merge 1 commit intokutovoys:mainfrom
yagee:main
Closed

fix: ensure proxy stable IDs are unique for distinct configs#137
yagee wants to merge 1 commit intokutovoys:mainfrom
yagee:main

Conversation

@yagee
Copy link
Copy Markdown

@yagee yagee commented Mar 30, 2026

Description of Changes

Fix proxy identity generation so distinct proxy configurations no longer receive the same stableId.

The previous stableId generation used only a subset of proxy fields, which caused collisions for configs that shared the same
protocol/server/port/UUID/public key but differed in meaningful transport fields such as Reality fingerprint. This broke the
dashboard because Alpine uses stableId as the x-for key, resulting in duplicate key warnings and missing server cards.

This PR:

  • expands GenerateStableID() to include additional identity-defining fields
  • ensures runtime stableId values remain unique even if two entries are otherwise identical
  • updates config equality checks to compare duplicate counts correctly instead of treating IDs as a set

Type of Changes

  • Bug fix
  • New feature
  • Documentation improvement
  • Performance optimization
  • Other: identity handling

Related Issues

Fixes #

Checklist

  • I have performed a self-review of my code
  • I have made corresponding changes to the documentation
  • I have tested changes locally

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes proxy identity generation so distinct proxy configurations no longer collide on stableId, preventing duplicate keys/missing proxy cards in the dashboard.

Changes:

  • Expands ProxyConfig.GenerateStableID() to include additional transport/security fields.
  • Ensures runtime StableID uniqueness by suffixing duplicate base IDs in PrepareProxyConfigs.
  • Updates config equality checks to compare multiplicities (counts) instead of treating IDs as a set.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
xray/utils.go Reworks stable ID assignment to ensure uniqueness and updates config equality logic to be count-based.
models/proxy_config.go Extends the stable ID hash inputs to cover more fields that define proxy identity.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread models/proxy_config.go

idComponents = append(idComponents, pc.Server)
idComponents = append(idComponents, fmt.Sprintf("%d", pc.Port))
idComponents = append(idComponents, pc.Name)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GenerateStableID() now incorporates pc.Name. Since Name is a user-facing label that can change without the underlying proxy transport/credentials changing (e.g., subscription providers often rename entries), this will change stable IDs and can trigger unnecessary config reloads / break any consumers that expect the ID to be stable across renames. Consider removing Name from the identity hash (or introducing a separate, explicitly "display" field) so identity is derived only from connection-defining fields.

Suggested change
idComponents = append(idComponents, pc.Name)

Copilot uses AI. Check for mistakes.
Comment thread models/proxy_config.go
Comment on lines +151 to 156
if len(pc.ALPN) > 0 {
idComponents = append(idComponents, strings.Join(pc.ALPN, ","))
}

idString := strings.Join(idComponents, "|")

Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idString is built by joining components with the literal "|", but several newly-added components (e.g., Name, Path, Host, ServiceName, potentially ALPN entries) can legally contain |. That makes the concatenation ambiguous and can reintroduce collisions where different component lists produce the same idString. Use an unambiguous encoding (e.g., length-prefix each component, or JSON encode a struct/array) before hashing.

Copilot uses AI. Check for mistakes.
Comment thread xray/utils.go
Comment on lines 34 to +39
for _, cfg := range old {
if cfg.StableID == "" {
cfg.StableID = cfg.GenerateStableID()
}
oldMap[cfg.StableID] = true
oldCounts[cfg.GenerateStableID()]++
}

for _, cfg := range new {
if cfg.StableID == "" {
cfg.StableID = cfg.GenerateStableID()
}
newMap[cfg.StableID] = true
newCounts[cfg.GenerateStableID()]++
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsConfigsEqual now compares configs via GenerateStableID() counts. If GenerateStableID() includes mutable/non-identity fields (notably Name per this PR), a pure rename will be treated as a config change and will trigger a full reconfigure/restart. Consider comparing on a dedicated identity function that excludes display-only fields, or ensure GenerateStableID() is strictly connection-defining.

Copilot uses AI. Check for mistakes.
Comment thread models/proxy_config.go
Comment on lines 120 to +124
idComponents = append(idComponents, pc.PublicKey)
}

if pc.Fingerprint != "" {
idComponents = append(idComponents, pc.Fingerprint)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GenerateStableID() is now used for config change detection (IsConfigsEqual) and for disambiguating entries, but it still omits some connection-impacting fields that are parsed and used in Xray config generation (e.g., AllowInsecure is set from subscription links and used in xray/config.go TLS stream settings). If AllowInsecure changes, IsConfigsEqual may incorrectly treat configs as unchanged and skip reconfiguration. Consider including AllowInsecure (and any other stream-settings fields that affect the generated Xray config) in the stable ID input.

Copilot uses AI. Check for mistakes.
@Vysokostnyi
Copy link
Copy Markdown

Hi there! I've been investigating the same Cannot read properties of undefined (reading 'after') crash in Alpine.js caused by StableID collisions.

While this PR improves things by adding more transport fields to the hash, it still fails when a subscription file (like the ones commonly found on GitHub) contains multiple entries for the same server with different names/fragments. Since Name isn't part of the identity in the current logic, they still receive duplicate IDs, which leads to the UI crash.

I’ve found that the most robust solution is to:

Save the original RawLink (the full URL string) inside the ProxyConfig model during parsing.
Generate the StableID by hashing this RawLink. This ensures that every unique line in the source file gets a unique ID, resolving all Alpine.js rendering issues.
Update the mapping in parser.go (specifically the originalData map) to use Server:Port:Name as a key instead of just Server:Port to avoid overwriting duplicate entries during parsing.
I've tested this approach locally, and it completely resolves the issue even with very "messy" subscription files.

@yagee yagee closed this Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants