mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 15:47:14 +00:00
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:
@@ -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))
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package xray
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/proxy/vless"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// typedReverse stands in for model.ClientReverse, which this package cannot
|
||||
// import (model imports xray); it marshals to the same {"tag":"…"} shape.
|
||||
type typedReverse struct {
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
// A reverse client's account has to carry its tag: removing the user drops the
|
||||
// outbound handler, and only the tag lets the core rebuild it (GetReverse).
|
||||
func TestBuildUserAccountCarriesTheReverseTag(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
reverse any
|
||||
want string
|
||||
}{
|
||||
{"settings json shape", map[string]any{"tag": "portal"}, "portal"},
|
||||
{"typed client field", &typedReverse{Tag: "portal"}, "portal"},
|
||||
{"blank tag", map[string]any{"tag": ""}, ""},
|
||||
{"typed nil", (*typedReverse)(nil), ""},
|
||||
{"absent", nil, ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
user := map[string]any{
|
||||
"email": "reverse@example.test",
|
||||
"id": "5f2eb9d6-3a2f-4a55-9812-6ea1e2f7a333",
|
||||
"reverse": tc.reverse,
|
||||
}
|
||||
tm, err := buildUserAccount("vless", user)
|
||||
if err != nil {
|
||||
t.Fatalf("buildUserAccount: %v", err)
|
||||
}
|
||||
if tm == nil {
|
||||
t.Fatal("buildUserAccount returned no account for vless")
|
||||
}
|
||||
account := new(vless.Account)
|
||||
if err := proto.Unmarshal(tm.Value, account); err != nil {
|
||||
t.Fatalf("unmarshal vless account: %v", err)
|
||||
}
|
||||
if got := account.GetReverse().GetTag(); got != tc.want {
|
||||
t.Fatalf("the account carries reverse tag %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user