From d175050f2e2cd91d05a84edebcc53dfa57b9b2cd Mon Sep 17 00:00:00 2001 From: Sanaei Date: Mon, 24 Aug 2026 13:27:40 +0200 Subject: [PATCH] fix(job): force-disconnect over-limit Hysteria2 clients disconnectClientTemporarily still gated on the protocol list from before XrayAPI.AddUser learned hysteria, so an over-limit Hysteria2 client kept its QUIC session until the fail2ban ban aged out, while a VLESS client in the same situation was dropped at once. buildUserAccount handles hysteria and model.Client already marshals the auth field the re-add needs, so admit the protocol. wireguard stays excluded: its keepAlive marshals as a JSON number, which the string-only user-field parsing rejects after the user was already removed. Closes #6256 --- internal/web/job/check_client_ip_job.go | 5 ++- internal/web/job/limit_ip_disconnect_test.go | 42 ++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 internal/web/job/limit_ip_disconnect_test.go diff --git a/internal/web/job/check_client_ip_job.go b/internal/web/job/check_client_ip_job.go index 8dc547e5d..93a8cef12 100644 --- a/internal/web/job/check_client_ip_job.go +++ b/internal/web/job/check_client_ip_job.go @@ -634,10 +634,11 @@ func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, c return } - // Only perform remove/re-add for protocols supported by XrayAPI.AddUser + // Protocols XrayAPI can remove and re-add from a marshaled model.Client. + // wireguard stays out: keepAlive marshals as a number, AddUser wants a string. protocol := string(inbound.Protocol) switch protocol { - case "vmess", "vless", "trojan", "shadowsocks": + case "vmess", "vless", "trojan", "shadowsocks", "hysteria": // supported protocols, continue default: logger.Warningf("[LIMIT_IP] Temporary disconnect is not supported for protocol %s on inbound %s", protocol, inbound.Tag) diff --git a/internal/web/job/limit_ip_disconnect_test.go b/internal/web/job/limit_ip_disconnect_test.go new file mode 100644 index 000000000..2d8012375 --- /dev/null +++ b/internal/web/job/limit_ip_disconnect_test.go @@ -0,0 +1,42 @@ +package job + +import ( + "strings" + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/database/model" + "github.com/mhsanaei/3x-ui/v3/internal/logger" +) + +// The protocol gate must let hysteria through: XrayAPI supports it, and the +// skip left over-limit Hysteria2 sessions alive until the fail2ban ban caught up. +func TestDisconnectClientTemporarilyAllowsHysteria(t *testing.T) { + setupIntegrationDB(t) + + const email = "hy2-limit-probe" + inbound := &model.Inbound{ + Id: 1, + Protocol: model.Hysteria, + Tag: "hy2-limit-probe-tag", + Settings: `{"clients":[]}`, + } + clients := []model.Client{{Email: email, Auth: "secret"}} + + (&CheckClientIpJob{}).disconnectClientTemporarily(inbound, email, clients) + + var unsupported, attempted bool + for _, line := range logger.GetLogs(500, "warning") { + if strings.Contains(line, "Temporary disconnect is not supported for protocol hysteria") { + unsupported = true + } + if strings.Contains(line, "Failed to remove user "+email) { + attempted = true + } + } + if unsupported { + t.Fatal("hysteria was rejected by the protocol gate") + } + if !attempted { + t.Fatal("expected a remove attempt against the Xray API for hysteria") + } +}