Files
3x-ui/internal/xray/api_reverse_test.go
T
BlindMaster24 cfa8350d10 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.
2026-09-15 18:00:54 +03:00

55 lines
1.5 KiB
Go

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)
}
})
}
}