mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-28 20:56:42 +08:00
6ac0c88084
* fix(xray): hot-apply Hysteria client changes without replacing the inbound diffInboundUsers only allowed per-user AlterInbound ops for vless, vmess and trojan. For a hysteria inbound every client add/remove/update became DelInbound + AddInbound: the UDP listener was recreated and all QUIC sessions of that inbound were lost. quic-go sends no stateless reset, so every connected client stalled until its idle timeout (30s by default) after each unrelated client mutation. XrayAPI.AddUser already builds a hysteria account and Xray-core's hysteria server implements AddUser/RemoveUser, so adding the protocol to userDiffableProtocols is sufficient. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(xray): say which branch each drop-guard protocol takes With hysteria in userDiffableProtocols its dropped client reaches the guard through the per-user diff, so the test named for protocols the diff cannot handle no longer described its hysteria case. --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
114 lines
3.5 KiB
Go
114 lines
3.5 KiB
Go
package xray
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
|
|
)
|
|
|
|
func hotConfigWithClients(clients string) *Config {
|
|
cfg := makeHotConfig()
|
|
for i := range cfg.InboundConfigs {
|
|
if cfg.InboundConfigs[i].Tag == "inbound-1080" {
|
|
cfg.InboundConfigs[i].Settings = json_util.RawMessage(`{"clients":` + clients + `}`)
|
|
}
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// Shadowsocks reaches the drop guard through the inbound's settings.clients compare,
|
|
// hysteria through its per-user diff; either way a dropped client must be reported.
|
|
func TestHotDiffDropsUsersOnShadowsocksAndHysteria(t *testing.T) {
|
|
for _, protocol := range []string{"shadowsocks", "hysteria"} {
|
|
t.Run(protocol, func(t *testing.T) {
|
|
withClients := func(clients string) *Config {
|
|
cfg := makeHotConfig()
|
|
ib := &cfg.InboundConfigs[1]
|
|
ib.Protocol = protocol
|
|
ib.Settings = json_util.RawMessage(`{"clients":` + clients + `}`)
|
|
return cfg
|
|
}
|
|
both := `[{"email":"a@x","password":"pa"},{"email":"b@x","password":"pb"}]`
|
|
onlyA := `[{"email":"a@x","password":"pa"}]`
|
|
|
|
diff, ok := ComputeHotDiff(withClients(both), withClients(onlyA))
|
|
if !ok {
|
|
t.Fatal("a dropped client must stay API-applicable")
|
|
}
|
|
if !diff.DropsUsers() {
|
|
t.Fatalf("DropsUsers = false for a dropped %s client (removed=%+v added=%+v dropped=%+v)",
|
|
protocol, diff.RemovedUsers, diff.AddedUsers, diff.DroppedClients)
|
|
}
|
|
|
|
edited, ok := ComputeHotDiff(withClients(both), withClients(`[{"email":"a@x","password":"pa"},{"email":"b@x","password":"pb","level":1}]`))
|
|
if !ok {
|
|
t.Fatal("a client edit must stay API-applicable")
|
|
}
|
|
if edited.DropsUsers() {
|
|
t.Fatal("an edited client is still served")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A disable or a delete takes the client out of the generated config; an edit
|
|
// keeps the email and re-adds it. Only the first leaves sessions running.
|
|
func TestHotDiffDropsUsers(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
old string
|
|
new string
|
|
want bool
|
|
}{
|
|
{
|
|
"client taken out of the config",
|
|
`[{"email":"a@x","id":"11111111-1111-1111-1111-111111111111","enable":true}]`,
|
|
`[]`,
|
|
true,
|
|
},
|
|
{
|
|
"edited in place",
|
|
`[{"email":"a@x","id":"11111111-1111-1111-1111-111111111111","enable":true}]`,
|
|
`[{"email":"a@x","id":"11111111-1111-1111-1111-111111111111","limitIp":5,"enable":true}]`,
|
|
false,
|
|
},
|
|
{
|
|
"added",
|
|
`[]`,
|
|
`[{"email":"a@x","id":"11111111-1111-1111-1111-111111111111","enable":true}]`,
|
|
false,
|
|
},
|
|
{
|
|
"renamed",
|
|
`[{"email":"a@x","id":"11111111-1111-1111-1111-111111111111","enable":true}]`,
|
|
`[{"email":"b@x","id":"11111111-1111-1111-1111-111111111111","enable":true}]`,
|
|
true,
|
|
},
|
|
{
|
|
"one dropped, one edited",
|
|
`[{"email":"a@x","id":"11111111-1111-1111-1111-111111111111","enable":true},{"email":"b@x","id":"22222222-2222-2222-2222-222222222222","enable":true}]`,
|
|
`[{"email":"b@x","id":"22222222-2222-2222-2222-222222222222","limitIp":5,"enable":true}]`,
|
|
true,
|
|
},
|
|
{
|
|
"unchanged",
|
|
`[{"email":"a@x","id":"11111111-1111-1111-1111-111111111111","enable":true}]`,
|
|
`[{"email":"a@x","id":"11111111-1111-1111-1111-111111111111","enable":true}]`,
|
|
false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
diff, ok := ComputeHotDiff(hotConfigWithClients(tc.old), hotConfigWithClients(tc.new))
|
|
if !ok {
|
|
t.Fatalf("diff of %s -> %s must be API-applicable", tc.old, tc.new)
|
|
}
|
|
if got := diff.DropsUsers(); got != tc.want {
|
|
t.Fatalf("DropsUsers = %v, want %v (removed=%+v added=%+v)",
|
|
got, tc.want, diff.RemovedUsers, diff.AddedUsers)
|
|
}
|
|
})
|
|
}
|
|
}
|