fix(xray): reconcile client auto-disable through the API instead of a forced restart

When a client expired or hit its traffic limit, XrayTrafficJob called
RestartXray(true), stopping the whole process and dropping every live
connection on every inbound (#5712 reported this as XHTTP on 443 dying) —
even though disableInvalidClients had already removed the user from the
running core over gRPC. The force restart existed only to re-sync the
process's config snapshot.

Switch the job to a non-forced restart and teach ComputeHotDiff to express
a client-only inbound change as per-user AlterInbound operations for
vless/vmess/trojan, so the reconcile is a no-op RemoveUser plus a snapshot
update rather than a handler swap that would still blip that inbound's
listener. Anything beyond the clients list still falls back to handler
replacement or a full restart as before.

Closes #5712
This commit is contained in:
MHSanaei
2026-07-02 09:26:53 +02:00
parent 1153d5db8c
commit e5b56c9444
5 changed files with 220 additions and 4 deletions
+2 -2
View File
@@ -50,8 +50,8 @@ func (j *XrayTrafficJob) Run() {
logger.Warning("get RestartXrayOnClientDisable failed:", settingErr)
}
if restartOnDisable {
if err := j.xrayService.RestartXray(true); err != nil {
logger.Warning("restart xray after disabling clients failed:", err)
if err := j.xrayService.RestartXray(false); err != nil {
logger.Warning("reconcile xray after disabling clients failed:", err)
j.xrayService.SetToNeedRestart()
}
}
+25
View File
@@ -1004,6 +1004,12 @@ func (s *XrayService) tryHotApply(newCfg *xray.Config) bool {
// Removals first so changed handlers and port swaps never collide with
// the additions that follow.
for _, u := range diff.RemovedUsers {
if err := hotAPI.RemoveUser(u.Tag, u.Email); err != nil && !xray.IsMissingHandlerErr(err) {
logger.Info("hot apply: remove user [", u.Email, "] from [", u.Tag, "] failed:", err)
return false
}
}
for _, tag := range diff.RemovedInboundTags {
if err := hotAPI.DelInbound(tag); err != nil && !xray.IsMissingHandlerErr(err) {
logger.Info("hot apply: remove inbound [", tag, "] failed:", err)
@@ -1028,6 +1034,12 @@ func (s *XrayService) tryHotApply(newCfg *xray.Config) bool {
return false
}
}
for _, u := range diff.AddedUsers {
if err := addUserReconciling(&hotAPI, u); err != nil {
logger.Info("hot apply: add user [", u.Email, "] to [", u.Tag, "] failed:", err)
return false
}
}
if diff.RoutingConfig != nil {
if err := hotAPI.ApplyRoutingConfig(diff.RoutingConfig); err != nil {
logger.Info("hot apply: apply routing config failed:", err)
@@ -1039,6 +1051,19 @@ func (s *XrayService) tryHotApply(newCfg *xray.Config) bool {
return true
}
// addUserReconciling adds a user, and on an email conflict (the user was
// already applied through the runtime API) replaces the existing user instead.
func addUserReconciling(api *xray.XrayAPI, u xray.UserOp) error {
err := api.AddUser(u.Protocol, u.Tag, u.User)
if err == nil || !xray.IsUserExistsErr(err) {
return err
}
if delErr := api.RemoveUser(u.Tag, u.Email); delErr != nil && !xray.IsMissingHandlerErr(delErr) {
return delErr
}
return api.AddUser(u.Protocol, u.Tag, u.User)
}
// addInboundReconciling adds an inbound, and on a tag conflict (the handler
// was already created through the runtime API while the stored snapshot was
// stale) replaces the existing handler instead.