fix(clients): keep a vless reverse client's handler across a re-add (#6558)

* fix(clients): keep a vless reverse client's handler across a re-add

RemoveUser also drops the client's reverse outbound handler, and the account
every live remove/re-add path rebuilt carried no reverse at all: buildUserAccount
read id/flow/testseed/testpre and nothing else. Editing, bulk re-enabling, quota
renewal and adding a client to an existing inbound therefore left a reverse
client able to connect but not to open its tunnel until Xray restarted, with
nothing logged. A traffic reset is the route operators hit most, since a
depleted client is removed and re-added on every renewal.

buildUserAccount now carries the tag (it accepts either the settings JSON object
or a typed client value), and the five account maps those paths build include
the client's reverse. Core chain, read from the pinned xray-core:
AddUserOperation -> User.ToMemoryUser -> vless.Account.AsAccount copies Reverse
(proxy/vless/account.go:24), and GetReverse rebuilds the handler from the stored
account's tag (proxy/vless/inbound/inbound.go:193-205).

Each path has a test that fails without its fix; the account-level test fails on
both input shapes.

* refactor(clients): drop an account map helper nothing calls

Local.AddClient and Local.UpdateUser are only reachable through runtime.Runtime,
and all four call sites of those two methods sit in a node branch, where the
runtime is a *Remote -- Remote.AddUser ignores the map and pushes the inbound
snapshot instead. So the extraction and its test covered a path no deployment
takes, the reverse key it added could never reach a core, and the previous
commit's claim that the node-push paths go through it was wrong.

The four account maps that do reach buildUserAccount are untouched. Reported by
the PR review.
This commit is contained in:
BlindMaster24
2026-09-15 18:00:54 +03:00
committed by GitHub
parent af466b6a24
commit cfa8350d10
6 changed files with 231 additions and 0 deletions
+24
View File
@@ -496,6 +496,25 @@ func shadowsocksCipherName(user map[string]any) (string, error) {
return getOptionalUserString(user, "method")
}
// reverseTag reads a vless reverse proxy tag from either shape a caller can
// carry: the settings JSON object, or a typed client value marshalling alike.
func reverseTag(value any) string {
if value == nil {
return ""
}
raw, err := json.Marshal(value)
if err != nil {
return ""
}
var parsed struct {
Tag string `json:"tag"`
}
if json.Unmarshal(raw, &parsed) != nil {
return ""
}
return parsed.Tag
}
// shadowsocksCipherType mirrors xray-core's infra/conf cipherFromString,
// aliases and case-insensitivity included, so the account the panel builds for
// a live user matches the one the core built for that inbound from its config.
@@ -556,6 +575,11 @@ func buildUserAccount(protocolName string, user map[string]any) (*serial.TypedMe
Id: userID,
Flow: userFlow,
}
// RemoveUser also drops the account's reverse outbound handler, and
// GetReverse only rebuilds it from the tag a re-added account carries.
if tag := reverseTag(user["reverse"]); tag != "" {
vlessAccount.Reverse = &vless.Reverse{Tag: tag}
}
if testseedVal, ok := user["testseed"]; ok {
if testseedArr, ok := testseedVal.([]any); ok && len(testseedArr) >= 4 {
testseed := make([]uint32, len(testseedArr))