Skip to content

xray: within one user batch the last entry for a user does not win #129

Open

Description

@Free-Guy-IR

Found by code review and reproduced in a lab; never seen in production. This is a robustness fix: the upstream panel and bridge do not send such a batch today (see below).

Summary

When one batch lists the same email more than once, the Xray backend does not apply the last entry. UpdateUsers (reached through SyncUsersChunked, controller/users_chunk.go:37) compares every entry with the state from before the batch and applies all removals before any add, and Config.syncUsers (full sync and startup config) never takes off a user that an earlier entry added. The result follows an earlier entry.

Reproduction (real node, real Xray)

Real node built from dev 9ccd2638c7fe, REST, real Xray 26.3.27, macOS arm64; one trojan inbound; core state read with xray api inbounduser. Each step is one PUT /users/sync/chunked with one chunk:

batch for 1.alice before last entry wants core after, dev 9ccd2638
[on trojan, no inbounds] (added then removed) absent absent present
[no inbounds, on trojan] (removed then kept) present present absent

All requests answered HTTP 200. The core keeps the wrong state until a later sync for that user (read from the code, not reproduced).

Not reachable from the upstream panel or bridge today

Found by reading, not run against a real panel. node_bridge_py c5632b65a10a queues one entry per email per node (PasarGuardNodeBridge/storage.py:159-166, pending[user.email] = ...) before it sends SyncUser / SyncUsersChunked, and panel dev f92acaf51cec sends users through the bridge (app/node/__init__.py:293,415,428). Any other client of the node's gRPC or REST API can send such a batch (read from the code; the lab used REST).

Config.updateUsers has the same ordering, but no RPC reaches it: its only caller is UpdateUsersAndRestart (backend/xray/user.go:439), and nothing outside the backends calls that (git grep at 9ccd2638).

Root cause (dev 9ccd2638)

  • backend/xray/config.go:112 (buildInboundUpdates) handles every entry, so one email can be both in update.accounts and in removeEmailSet.
  • backend/xray/user.go:387 changedAccounts compares each entry with the pre-batch state; user.go:392 applies every removal before the adds at user.go:407.
  • backend/xray/config.go:66 Config.syncUsers -> Inbound.syncUsers (:143) only adds users that list the inbound.

Affected versions

dev 9ccd2638c7fe and main v0.5.7 739eab94a934.

Proposed fix

Keep only the last entry per email before building the per-inbound updates and before rebuilding an inbound's clients. PR #134.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions