mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 03:05:02 +03:00
fix(node): apply a pushed inbound's client changes in place instead of rebuilding it
A master pushes the whole inbound to its node whenever a client on it is auto-disabled, renewed or reset, and the node-sync reconcile re-sends every inbound whose payload changed (all of them after a master restart). The node's UpdateInbound answered every push with DelInbound + AddInbound, so Xray closed the listener and every open connection on that port dropped. On a node inbound with ~2,700 clients that was dozens of times a day. UpdateInbound now diffs the config Xray is running against the new one. When only settings.clients differ (vless, vmess, trojan, hysteria, masque) it applies per-user RemoveUser/AddUser through the runtime, and when nothing the core sees changed it does nothing. Any other change, or any per-user failure (a handler the core lost included), still rebuilds as before. AmneziaWG joins MTProto and TUIC on rt.UpdateInbound, whose in-place Ensure keeps the device and its peers; Del+Add tore it down. A pushed removal does not consult the node's own restartXrayOnClientDisable: it defaults to on, and the master already restarts its nodes from its own setting (restartRemoteNodesOnDisable). Nodes on an older release keep rebuilding until they update; the master side needs no change. Verified: the new service tests, TestXrayAPI_E2E_Users against a real core (pins the missing-user vs missing-handler error texts) and make node-e2e. make race not run: no goroutine, lock or shared state changed. Closes #6795
This commit is contained in:
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/maskcompat"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/netsafe"
|
||||
wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -1964,6 +1965,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
if err := tx.First(stored, inbound.Id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
running := *stored
|
||||
oldInbound = stored
|
||||
// The form posts back the clients and enable it loaded; both have their
|
||||
// own endpoints, so only a master's push may change them here.
|
||||
@@ -2113,7 +2115,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
}
|
||||
if !push {
|
||||
needRestart = true
|
||||
} else if oldProtocol == model.MTProto || oldInbound.Protocol == model.MTProto || oldProtocol == model.TUIC || oldInbound.Protocol == model.TUIC {
|
||||
} else if servedOutsideXray(oldProtocol) || servedOutsideXray(oldInbound.Protocol) {
|
||||
oldSnapshot := *oldInbound
|
||||
oldSnapshot.Tag = tag
|
||||
oldSnapshot.Protocol = oldProtocol
|
||||
@@ -2127,7 +2129,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
pushable = false
|
||||
}
|
||||
}
|
||||
newProtocolIsSidecar := oldInbound.Protocol == model.MTProto || oldInbound.Protocol == model.TUIC
|
||||
newProtocolIsSidecar := servedOutsideXray(oldInbound.Protocol)
|
||||
if pushable {
|
||||
postCommitApply = func() {
|
||||
if err2 := rt.UpdateInbound(context.Background(), &oldSnapshot, payload); err2 == nil {
|
||||
@@ -2143,7 +2145,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
} else {
|
||||
oldSnapshot := *oldInbound
|
||||
oldSnapshot.Tag = tag
|
||||
var runtimeInbound *model.Inbound
|
||||
var runtimeInbound, runningInbound *model.Inbound
|
||||
if inbound.Enable {
|
||||
var err2 error
|
||||
runtimeInbound, err2 = s.buildInboundForLocalRuntime(tx, oldInbound)
|
||||
@@ -2152,7 +2154,15 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
needRestart = true
|
||||
}
|
||||
}
|
||||
if runtimeInbound != nil && running.Enable {
|
||||
// Unbuildable means unknown running state: the rebuild below covers it.
|
||||
runningInbound, _ = s.buildInboundForLocalRuntime(tx, &running)
|
||||
}
|
||||
postCommitApply = func() {
|
||||
if runningInbound != nil && applyInboundUserOps(rt, runningInbound, runtimeInbound) {
|
||||
logger.Debug("Inbound clients updated in place on", rt.Name(), ":", oldInbound.Tag)
|
||||
return
|
||||
}
|
||||
if err2 := rt.DelInbound(context.Background(), &oldSnapshot); err2 == nil {
|
||||
logger.Debug("Old inbound deleted on", rt.Name(), ":", tag)
|
||||
}
|
||||
@@ -2217,6 +2227,41 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
return inbound, needRestart, nil
|
||||
}
|
||||
|
||||
// servedOutsideXray: these protocols run under their own manager, whose update
|
||||
// reconfigures in place where a Del+Add would drop every live session.
|
||||
func servedOutsideXray(protocol model.Protocol) bool {
|
||||
return protocol == model.MTProto || protocol == model.TUIC || protocol == model.AmneziaWG
|
||||
}
|
||||
|
||||
// applyInboundUserOps reports whether per-user core ops moved running to next, so
|
||||
// its listener and live connections survive; false means rebuild the handler.
|
||||
func applyInboundUserOps(rt runtime.Runtime, running, next *model.Inbound) bool {
|
||||
diff, ok := xray.InboundUserOps(running.GenXrayInboundConfig(), next.GenXrayInboundConfig())
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
ctx := context.Background()
|
||||
for _, u := range diff.RemovedUsers {
|
||||
if err := rt.RemoveUser(ctx, next, u.Email); err != nil && !xray.IsMissingUserErr(err) {
|
||||
logger.Debug("Unable to remove client", u.Email, "in place on", rt.Name(), ":", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, u := range diff.AddedUsers {
|
||||
err := rt.AddUser(ctx, next, u.User)
|
||||
if xray.IsUserExistsErr(err) {
|
||||
if err = rt.RemoveUser(ctx, next, u.Email); err == nil {
|
||||
err = rt.AddUser(ctx, next, u.User)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
logger.Debug("Unable to add client", u.Email, "in place on", rt.Name(), ":", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// A node mirrors this payload into its own DB, so every client must survive:
|
||||
// filtering one out makes the node delete it, and the master then mirrors that.
|
||||
func (s *InboundService) buildInboundForNodePush(tx *gorm.DB, inbound *model.Inbound) (*model.Inbound, error) {
|
||||
|
||||
Reference in New Issue
Block a user