fix(nodes): propagate single-client deletion to remote nodes (#5352)

Deleting a client attached to a remote-node inbound could silently fail
to reach the node, so the node's next traffic snapshot resurrected the
client once the 90s delete tombstone expired.

Two paths in the single-client delete (Delete -> DelInboundClientByEmail):

- A disabled client was skipped entirely: the node-propagation and
  mark-dirty block sat behind the client's enable flag (needApiDel), so a
  disabled client on a node never detached and never marked the node
  dirty. The bulk and multi-client delete paths already handle the node
  case independently of enable state; mirror that structure here.

- Remote.DeleteUser returned nil when resolveRemoteID failed, hiding the
  failure from the caller so the node was never marked dirty. Surface the
  error like AddClient/UpdateUser do, so the caller marks the node dirty
  and the next reconcile converges.

Add a regression test asserting a disabled node client's deletion marks
the node dirty.
This commit is contained in:
MHSanaei
2026-06-15 17:56:12 +02:00
parent cf5f37e409
commit cbb21b7575
3 changed files with 70 additions and 11 deletions
+24 -10
View File
@@ -742,15 +742,17 @@ func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inbo
}
}
if needApiDel {
rt, push, dirty, perr := inboundSvc.nodePushPlan(oldInbound)
if perr != nil {
return false, perr
}
if dirty {
markDirty = true
}
if oldInbound.NodeID == nil {
if oldInbound.NodeID == nil {
// Local inbound: a disabled client isn't in the running Xray, so only
// a live one (needApiDel) needs an API removal.
if needApiDel {
rt, push, dirty, perr := inboundSvc.nodePushPlan(oldInbound)
if perr != nil {
return false, perr
}
if dirty {
markDirty = true
}
if !push {
needRestart = true
} else if err1 := rt.RemoveUser(context.Background(), oldInbound, email); err1 == nil {
@@ -762,7 +764,19 @@ func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inbo
logger.Debug("Error in deleting client on", rt.Name(), ":", email)
needRestart = true
}
} else if push {
}
} else {
// Node inbound: propagate the delete regardless of the enable flag —
// the node's own DB still carries a disabled client and would
// resurrect it on the next snapshot otherwise.
rt, push, dirty, perr := inboundSvc.nodePushPlan(oldInbound)
if perr != nil {
return false, perr
}
if dirty {
markDirty = true
}
if push {
if err1 := rt.DeleteUser(context.Background(), oldInbound, email); err1 != nil {
logger.Warning("Error in deleting client on", rt.Name(), ":", err1)
markDirty = true