Skip to content

fix(xray): leave unchanged users alone during user sync - #93

Merged
ImMohammad20000 merged 2 commits into
PasarGuard:devfrom
Free-Guy-IR:fix/xray-sync-only-changed-users
Sep 24, 2026
Merged

ImMohammad20000 merged 2 commits into
PasarGuard:devfrom
Free-Guy-IR:fix/xray-sync-only-changed-users

Conversation

@Free-Guy-IR

@Free-Guy-IR Free-Guy-IR commented Sep 23, 2026 •

Copy link
Copy Markdown

Summary

SyncUser and UpdateUsers used to remove a user from every inbound they stay on and then add them again. They did this on every call, even when the account had not changed. With this PR, the node sends that remove and add only when the user's account on that inbound changed. Users who leave an inbound are removed exactly as before.

Refs #92.

Why this matters now. The node takes access away with Xray's RemoveUserOperation. A removed user keeps the connections they already have open (#92). XTLS/Xray-core#5844 makes RemoveUser close those connections. With that core, the current remove and add would disconnect a user every time the panel sends them again without changes, for example after an admin edits their data limit or expiry date. With this PR, only two kinds of user have their connections closed: users who lose access to an inbound, and users whose credentials changed.

On today's stock core users see no difference, and the node sends fewer gRPC calls. When a user who is on N inbounds is synced without changes, the node now sends 2N fewer calls.

Details

  • Inbound.hasAccount and Inbound.changedAccounts compare the account the node is about to send with the one it stored for that inbound. They use reflect.DeepEqual. syncUsers, SyncUser and UpdateUsers build the stored account with the same constructors, including the Shadowsocks 2022 key normalisation, so a user loaded at startup counts as unchanged too.
  • If adding an account fails, the node now drops that account from its local state, so the next sync sees it as changed and tries again. Before this PR, the account stayed in the local state; with the new check, later syncs would have skipped it. The error is still returned to the panel, as before.
  • SyncUsers and UpdateUsersAndRestart are unchanged, since they restart the core with the full user list.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Refactor / cleanup
  • Documentation
  • Tests / CI

Checklist

  • I tested the change locally or explained why it cannot be tested.
  • I added or updated tests for behavior changes.
  • I updated documentation, translations, or examples if needed. (Not needed.)
  • I checked database migrations when models or schema changed. (No models or schema are involved.)
  • I did not include secrets, tokens, private keys, or unrelated changes.

Testing

New tests

  • backend/xray/user_sync_test.go (7 tests) runs SyncUser and UpdateUsers against a recording gRPC HandlerService. The config has one inbound for each protocol: vless, vmess, trojan, shadowsocks, shadowsocks 2022 and hysteria. The tests check exactly which operations reach Xray:
    • An unchanged user gets none, including a user loaded at startup.
    • A changed account is replaced only on its own inbound.
    • A user who leaves an inbound is removed from it.
    • An add that failed is retried on the next sync.
  • backend/xray/user_session_test.go runs the real core with a VLESS TCP inbound and a minimal VLESS client:
    • An open connection must keep flowing after SyncUser, and then UpdateUsers, send the same user again.
    • A removed user must not be able to open a new connection.
    • The test also logs how much the removed user's already open connection still receives.

Fails before, passes after

Go 1.26.3, linux/amd64, in an Alpine container limited to 1.5 CPUs:

Core dev (d44ca8e) This branch
Xray v26.3.27, official release 7 of 8 fail: every recording test sees a remove and an add for users that did not change. The session test passes, and the removed user's open connection receives 533 MB in the next second. 8 of 8 pass. The removed user's open connection still receives 568 MB in the next second, because the stock core never closes it (#92).
Head of XTLS/Xray-core#5844 (f356a34) 8 of 8 fail. The session test fails with open connection stopped after SyncUser sent the same user again: received 0 bytes, err EOF. 8 of 8 pass. The removed user's open connection is closed (EOF).

Full suite

The full suite was run as in run-tests.yml: Go 1.26.3, the official Xray v26.3.27 in /usr/local/bin/xray, and certificates from make generate_server_cert and make generate_client_cert.

  • backend/wireguard, backend/xray, backend/xray/api, controller/rest, controller/rpc, pkg/stats and pkg/sysstats all pass: 162 tests.
  • go vet ./... reports nothing except the controller problem described in the notes below.
go test ./backend/xray -run 'TestSyncUser|TestUpdateUsers|TestUserSyncKeepsAnUnchangedUsersOpenConnection' -v
TEST_INTEGRATION=true go test ./... -v -p 1

Screenshots

Not applicable.

Notes for reviewers

  • controller tests do not compile on dev. Since d44ca8e, controller/controller_test.go still calls keepAliveStale with three arguments. The "Test Modules" run for that push failed on this. This PR does not touch that file, so the regular test step fails the same way until it is fixed.
  • Overlap with Harden node lifecycle and API handling #78. Harden node lifecycle and API handling #78 rewrites the same lines of SyncUser and UpdateUsers, and it also records an account only after a successful add. Whichever PR lands second needs a small rebase. The two changes do not conflict in behaviour.
  • Race detector. Under -race, TestUserSyncKeepsAnUnchangedUsersOpenConnection reports the existing race between Core.Started and cmd.Wait (core.go:120 and core.go:257). TestXrayBackend reports the same race on unmodified dev, and fix(xray): synchronize process status checks #86 fixes it. The new recording tests are race-free.
  • This PR does not close a removed user's connections on its own. That needs a core with Add UserConnTracker to manage user connection cancellations across proxies XTLS/Xray-core#5844 or an equivalent change. This PR makes sure that, once the core does close them, only users who lost access are disconnected.
  • Changed credentials. A user whose credentials change while they stay on an inbound is still removed and added again. With such a core, their old connections are closed too. This is intended: old credentials should stop working.
  • Test config. The session test allows 127.0.0.1 in the freedom outbound's finalRules, because Xray 26.6 and later refuse private targets by default. Older cores ignore the field.

Summary by CodeRabbit

  • Bug Fixes
    • User syncs now leave unchanged accounts in place, helping existing connections continue without unnecessary interruptions.
    • Changes to one account are applied without disrupting other accounts on the same inbound.
    • User and account removals are handled more precisely during updates, and failed account additions are cleaned up for retry.

SyncUser and UpdateUsers removed and re-added every user on every inbound they stay on, even when nothing about the account changed. On a core that closes a user's connections when the user is removed, every such sync would disconnect the user.

A user is now removed and added again only when their account on that inbound changed. Users who leave an inbound are removed as before. An account whose add fails is dropped from the local state, so the next sync retries it.
Runs the real core with one VLESS inbound: an open connection must keep flowing after SyncUser and UpdateUsers resend the same user, and a removed user must not open a new connection. The test also logs how much the removed user's already open connection still received.
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 3ab7856f-7c8f-49d7-a179-4f0b2f566f92

📥 Commits

Reviewing files that changed from the base of the PR and between d44ca8e and 6e7fe4d.

📒 Files selected for processing (4)
  • backend/xray/config.go
  • backend/xray/user.go
  • backend/xray/user_session_test.go
  • backend/xray/user_sync_test.go

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


Walkthrough

Account synchronization now compares inbound accounts by email and deeply equal values. SyncUser and UpdateUsers use those comparisons to avoid operations for unchanged accounts. New tests cover synchronization operations, retries, and VLESS stream behavior.

Changes

Account synchronization

Layer / File(s) Summary
Account comparison and user synchronization
backend/xray/config.go, backend/xray/user.go, backend/xray/user_sync_test.go
Inbound account checks compare stored values with deep equality. SyncUser and UpdateUsers use the checks to select account operations. Tests verify unchanged accounts, changed accounts, removals, and retries.
VLESS stream synchronization checks
backend/xray/user_session_test.go
Tests check data flow on an open stream before and after unchanged-user synchronization. They also check that a new stream receives no bytes after the user leaves the inbound.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: m03ed

Merge Risk: ⚪ Minimal · up to 6e7fe

No actionable issue remains from this review; the change is ready for normal merge checks.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 20 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preserving unchanged users during Xray user synchronization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🧪 Generate unit tests (beta)
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks each account twice,
Deep equality keeps the list precise.
Unchanged streams keep sending cheer,
New streams find no user here.
The bunny hops through tests with glee!

Comment @coderabbitai help to get the list of available commands.

@ImMohammad20000
ImMohammad20000 merged commit be2c445 into PasarGuard:dev Sep 24, 2026
1 of 2 checks passed
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.

2 participants